Skip to content

fix(WDate): handle negative (BCE) years correctly#252

Open
saddamr3e wants to merge 2 commits into
emweb:masterfrom
saddamr3e:fix/wdate-negative-year-setYmd-ub-clean
Open

fix(WDate): handle negative (BCE) years correctly#252
saddamr3e wants to merge 2 commits into
emweb:masterfrom
saddamr3e:fix/wdate-negative-year-setYmd-ub-clean

Conversation

@saddamr3e

Copy link
Copy Markdown

While testing WDate with negative (BCE) years, I noticed that dates before the common era do not behave correctly.

The date is packed internally in WDate::setYmd() by shifting the year value and storing the result in an unsigned field. For negative years, this leads to incorrect behavior, causing BCE dates to compare incorrectly and making year values inconsistent.

This change preserves negative years correctly when packing the date and stores the packed value in a signed field. It also updates the validity check so BCE dates are handled consistently.

I also added a regression test to cover BCE dates and ensure comparisons and year extraction continue to work correctly.

WDate::setYmd packed the year into an unsigned int field using a
left-shift on a signed int (y << 16). When the year is negative
(BCE dates), this is undefined behavior in C++11/14/17. Even where
the platform produces a deterministic result, the unsigned storage
caused operator<() to return wrong results (a BCE date compared
greater than any CE date) and year() to return a garbage positive
value (e.g., 65535 for year -1).

The API explicitly supports negative years: fromJulianDay() produces
years as far back as -4713 (Julian Day epoch), and setDate() accepts
any year in [date::year::min(), date::year::max()] = [-32768, 32767].

Fix:
- Change ymd_ from unsigned to int so that operator<() performs a
  signed comparison (correct for BCE vs CE ordering) and year()
  returns the right value via arithmetic right-shift.
- In setYmd(), cast y to unsigned before shifting (shifting unsigned
  is always well-defined in C++) then static_cast<int> the result
  back into ymd_, preserving the two's-complement bit pattern.
- Fix isValid() from (ymd_ > 1) to (ymd_ != 0 && ymd_ != 1).
  The old guard relied on valid dates always being > 1, which holds
  for CE years but fails for BCE years whose packed ymd_ is negative.

All downstream methods (dayOfWeek, daysTo, toJulianDay, toTimePoint,
addDays, addMonths, addYears, toString, fromJulianDay, previousWeekday)
are fixed transitively since they all call year().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant