Skip to content

std/temporal: split Y - 1 into century and year in Date.dayOfWeek - #608

Open
notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/date-day-of-week
Open

notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/date-day-of-week

Conversation

@notactuallytreyanastasio

Copy link
Copy Markdown
Contributor

Date.dayOfWeek is wrong on the interpreter and on Rust for every year
ending in 00 that is not a multiple of 400 (1700, 1800, 1900, 2100, ...),
all year long. js, py, java, cpp and csharp connect dayOfWeek to their
own date code, so they were right; the interpreter, Rust and Lua run
std's Temper. The program:

let { Date } = import("std/temporal");
for (let y of [1600, 1700, 1800, 1900, 2000, 2100, 2200, 2300, 2400]) {
  console.log("${y}-01-01 ${new Date(y, 1, 1).dayOfWeek} ${y}-03-01 ${new Date(y, 3, 1).dayOfWeek} ${y}-12-31 ${new Date(y, 12, 31).dayOfWeek}");
}

at origin/main, temper run -b rust against -b js (py and java print
the same as js, and so does Python's datetime.isoweekday):

rust                                    js
1600-01-01 6 1600-03-01 3 1600-12-31 7  1600-01-01 6 1600-03-01 3 1600-12-31 7
1700-01-01 4 1700-03-01 7 1700-12-31 4  1700-01-01 5 1700-03-01 1 1700-12-31 5
1800-01-01 2 1800-03-01 5 1800-12-31 2  1800-01-01 3 1800-03-01 6 1800-12-31 3
1900-01-01 7 1900-03-01 3 1900-12-31 7  1900-01-01 1 1900-03-01 4 1900-12-31 1
2000-01-01 6 2000-03-01 3 2000-12-31 7  2000-01-01 6 2000-03-01 3 2000-12-31 7
2100-01-01 4 2100-03-01 7 2100-12-31 4  2100-01-01 5 2100-03-01 1 2100-12-31 5

and the interpreter, through temper repl:

1900-01-01 7 1900-03-01 3 2100-01-01 4 1700-01-01 4 2000-01-01 6

1900-01-01 was a Monday (1), not a Sunday (7).

Gauss's formula gives January first of year Y as
(1 + 5((Y-1) mod 4) + 4((Y-1) mod 100) + 6((Y-1) mod 400)) mod 7. std
split Y itself into century and year-of-century and made up for the
missing - 1 with offsets. That works until the year of the century is
00, because then Y - 1 is in the previous century. 2000 came out right
only because 2000 mod 400 is 0, and the existing tests (1596,
2010-2012, 2023) never hit a bad year.

It now takes the three remainders of Y - 1 directly, each made
non-negative by adding its divisor, since % keeps the dividend's sign,
so years before 1 still work. The month tables are unchanged. The
divisors are literals on purpose: a helper taking the divisor as a
parameter makes % able to bubble, and be-rust then refuses it
("Cannot translate Invalid").

After, the interpreter, rust, js, py and java all print the js column
above. On rust, every year from -1199 to 2999, for January 1, the last
day of February, March 1 and December 31, matches Python's datetime
(years before 1 compared through the 400-year cycle): 4199 lines, no
difference.

types/date now logs January and March first of 1700, 1800, 1900, 2000,
2100 and 2400. With the temporal change reverted it fails on the
interpreter and on rust with the left column, and passes on js. With it,
typesDate passes on the interpreter (InterpreterFunctionalTests), js,
py, rust, java 17 and cpp. :frontend:jvmTest (703 tests), ktlintCheck
and detekt pass.

What does not work: Lua is not installed here, so Lua, which also runs
this code, was not run. Separately, be-cpp's own Date::getDayOfWeek
returns 0 for every Sunday instead of 7 (2023-01-01 is 0 on cpp, 7 on
js); the new test has no Sunday in it, and this change does not touch
cpp.

🤖 Generated with Claude Code

`Date.dayOfWeek` is wrong on the interpreter and on Rust for every year
ending in 00 that is not a multiple of 400 (1700, 1800, 1900, 2100, ...),
all year long. js, py, java, cpp and csharp connect `dayOfWeek` to their
own date code, so they were right; the interpreter, Rust and Lua run
std's Temper. The program:

    let { Date } = import("std/temporal");
    for (let y of [1600, 1700, 1800, 1900, 2000, 2100, 2200, 2300, 2400]) {
      console.log("${y}-01-01 ${new Date(y, 1, 1).dayOfWeek} ${y}-03-01 ${new Date(y, 3, 1).dayOfWeek} ${y}-12-31 ${new Date(y, 12, 31).dayOfWeek}");
    }

at origin/main, `temper run -b rust` against `-b js` (py and java print
the same as js, and so does Python's `datetime.isoweekday`):

    rust                                    js
    1600-01-01 6 1600-03-01 3 1600-12-31 7  1600-01-01 6 1600-03-01 3 1600-12-31 7
    1700-01-01 4 1700-03-01 7 1700-12-31 4  1700-01-01 5 1700-03-01 1 1700-12-31 5
    1800-01-01 2 1800-03-01 5 1800-12-31 2  1800-01-01 3 1800-03-01 6 1800-12-31 3
    1900-01-01 7 1900-03-01 3 1900-12-31 7  1900-01-01 1 1900-03-01 4 1900-12-31 1
    2000-01-01 6 2000-03-01 3 2000-12-31 7  2000-01-01 6 2000-03-01 3 2000-12-31 7
    2100-01-01 4 2100-03-01 7 2100-12-31 4  2100-01-01 5 2100-03-01 1 2100-12-31 5

and the interpreter, through `temper repl`:

    1900-01-01 7 1900-03-01 3 2100-01-01 4 1700-01-01 4 2000-01-01 6

1900-01-01 was a Monday (1), not a Sunday (7).

Gauss's formula gives January first of year Y as
(1 + 5((Y-1) mod 4) + 4((Y-1) mod 100) + 6((Y-1) mod 400)) mod 7. std
split Y itself into century and year-of-century and made up for the
missing - 1 with offsets. That works until the year of the century is
00, because then Y - 1 is in the previous century. 2000 came out right
only because 2000 mod 400 is 0, and the existing tests (1596,
2010-2012, 2023) never hit a bad year.

It now takes the three remainders of Y - 1 directly, each made
non-negative by adding its divisor, since `%` keeps the dividend's sign,
so years before 1 still work. The month tables are unchanged. The
divisors are literals on purpose: a helper taking the divisor as a
parameter makes `%` able to bubble, and be-rust then refuses it
("Cannot translate Invalid").

After, the interpreter, rust, js, py and java all print the js column
above. On rust, every year from -1199 to 2999, for January 1, the last
day of February, March 1 and December 31, matches Python's `datetime`
(years before 1 compared through the 400-year cycle): 4199 lines, no
difference.

types/date now logs January and March first of 1700, 1800, 1900, 2000,
2100 and 2400. With the temporal change reverted it fails on the
interpreter and on rust with the left column, and passes on js. With it,
typesDate passes on the interpreter (InterpreterFunctionalTests), js,
py, rust, java 17 and cpp. :frontend:jvmTest (703 tests), ktlintCheck
and detekt pass.

What does not work: Lua is not installed here, so Lua, which also runs
this code, was not run. Separately, be-cpp's own `Date::getDayOfWeek`
returns 0 for every Sunday instead of 7 (2023-01-01 is 0 on cpp, 7 on
js); the new test has no Sunday in it, and this change does not touch
cpp.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Bobby Grayson <bobbbygrayson+github@gmail.com>
// `%` keeps the dividend's sign, so each remainder adds its
// divisor and takes it again, to stay in range for years before 1.
let y = year;
let c = if (y >= 0) { y / 100 } else { -(-y / 100) };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh. I probably meant to do something with ~ instead of - here.

1900: Jan 1 weekday 1, Mar 1 weekday 4
2000: Jan 1 weekday 6, Mar 1 weekday 3
2100: Jan 1 weekday 5, Mar 1 weekday 1
2400: Jan 1 weekday 6, Mar 1 weekday 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, here's an attempt to use date to double check these.

$ for y in {17,18,19,20,21,24}00; do echo "$y Jan 1 weekday $(date -j -f "%Y-%m-%d" $y-01-01 +%u), Mar 1 weekday $(date -j -f "%Y-%m-%d" $y-03-01 +%u)"; done
date: nonexistent time
date: nonexistent time
1700 Jan 1 weekday , Mar 1 weekday
date: nonexistent time
date: nonexistent time
1800 Jan 1 weekday , Mar 1 weekday
1900 Jan 1 weekday 1, Mar 1 weekday 4
2000 Jan 1 weekday 6, Mar 1 weekday 3
2100 Jan 1 weekday 5, Mar 1 weekday 1
2400 Jan 1 weekday 6, Mar 1 weekday 3

I'm guessing the nonexistent time thing is due to a failure to just use a proleptic Gregorian calendar.

My man cal says the below, so I'll not muck with that.

BUGS
The assignment of Julian–Gregorian switching dates to country codes is historically naive for many countries.

Java's java.time.LocalDate says:

The ISO-8601 calendar system is the modern civil calendar system used today in most of the world. It is equivalent to the proleptic Gregorian calendar system

And its DayOfWeek.getValue returns the ISO's 1-indexed weekday number starting at 1 = Monday.

Here's the output of jshell using that.

$ jshell
|  Welcome to JShell -- Version 21.0.4
|  For an introduction type: /help intro

jshell> import java.time.LocalDate;

jshell> for (int year : new int[] { 1700, 1800, 1900, 2000, 2100, 2400}) {
   ...>     LocalDate jan1 = LocalDate.of(year, 1, 1);
   ...>     LocalDate mar1 = LocalDate.of(year, 3, 1);
   ...>     System.err.println(year + ": Jan 1 weekday " + (jan1.getDayOfWeek().getValue()) + ", Mar 1 weekday " + (mar1.getDayOfWeek().getValue()));
   ...> }
1700: Jan 1 weekday 5, Mar 1 weekday 1
1800: Jan 1 weekday 3, Mar 1 weekday 6
1900: Jan 1 weekday 1, Mar 1 weekday 4
2000: Jan 1 weekday 6, Mar 1 weekday 3
2100: Jan 1 weekday 5, Mar 1 weekday 1
2400: Jan 1 weekday 6, Mar 1 weekday 3

jshell>

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.

2 participants