hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 9a62f94fc90ce92b4527b92e0e25a6c0d81d636f
parent 0e1a16ac41454af181f13c157bb5735052387b60
Author: Byron Torres <b@torresjrjr.com>
Date:   Wed, 17 May 2023 10:12:34 +0000

time::date: rename source files appropriately

Diffstat:
Mscripts/gen-stdlib | 28++++++++++++++--------------
Mstdlib.mk | 56++++++++++++++++++++++++++++----------------------------
Mtime/date/date.ha | 825++++++++++++++++++++++++++-----------------------------------------------------
Dtime/date/datetime.ha | 305------------------------------------------------------------------------------
Ctime/date/date.ha -> time/date/daydate.ha | 0
Rtime/date/time.ha -> time/date/daytime.ha | 0
Rtime/date/errors.ha -> time/date/error.ha | 0
Rtime/date/timezone.ha -> time/date/locality.ha | 0
Rtime/date/chronology.ha -> time/date/observe.ha | 0
Rtime/date/arithmetic.ha -> time/date/parithm.ha | 0
Rtime/date/duration.ha -> time/date/tarithm.ha | 0
11 files changed, 308 insertions(+), 906 deletions(-)

diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib @@ -1377,34 +1377,34 @@ time_chrono() { time_date() { gen_srcs -plinux time::date \ - arithmetic.ha \ - chronology.ha \ - errors.ha \ date.ha \ - datetime.ha \ - duration.ha \ + daydate.ha \ + daytime.ha \ + error.ha \ format.ha \ + locality.ha \ + observe.ha \ + parithm.ha \ parse.ha \ period.ha \ reckon.ha \ - time.ha \ - timezone.ha \ + tarithm.ha \ virtual.ha gen_ssa -plinux time::date \ ascii errors fmt io strconv strings strio time time::chrono gen_srcs -pfreebsd time::date \ - arithmetic.ha \ - chronology.ha \ - errors.ha \ date.ha \ - datetime.ha \ - duration.ha \ + daydate.ha \ + daytime.ha \ + error.ha \ format.ha \ + locality.ha \ + observe.ha \ + parithm.ha \ parse.ha \ period.ha \ reckon.ha \ - time.ha \ - timezone.ha \ + tarithm.ha \ virtual.ha gen_ssa -pfreebsd time::date \ ascii errors fmt io strconv strings strio time time::chrono diff --git a/stdlib.mk b/stdlib.mk @@ -2153,18 +2153,18 @@ $(HARECACHE)/time/chrono/time_chrono-freebsd.ssa: $(stdlib_time_chrono_freebsd_s # time::date (+linux) stdlib_time_date_linux_srcs = \ - $(STDLIB)/time/date/arithmetic.ha \ - $(STDLIB)/time/date/chronology.ha \ - $(STDLIB)/time/date/errors.ha \ $(STDLIB)/time/date/date.ha \ - $(STDLIB)/time/date/datetime.ha \ - $(STDLIB)/time/date/duration.ha \ + $(STDLIB)/time/date/daydate.ha \ + $(STDLIB)/time/date/daytime.ha \ + $(STDLIB)/time/date/error.ha \ $(STDLIB)/time/date/format.ha \ + $(STDLIB)/time/date/locality.ha \ + $(STDLIB)/time/date/observe.ha \ + $(STDLIB)/time/date/parithm.ha \ $(STDLIB)/time/date/parse.ha \ $(STDLIB)/time/date/period.ha \ $(STDLIB)/time/date/reckon.ha \ - $(STDLIB)/time/date/time.ha \ - $(STDLIB)/time/date/timezone.ha \ + $(STDLIB)/time/date/tarithm.ha \ $(STDLIB)/time/date/virtual.ha $(HARECACHE)/time/date/time_date-linux.ssa: $(stdlib_time_date_linux_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_time_chrono_$(PLATFORM)) @@ -2175,18 +2175,18 @@ $(HARECACHE)/time/date/time_date-linux.ssa: $(stdlib_time_date_linux_srcs) $(std # time::date (+freebsd) stdlib_time_date_freebsd_srcs = \ - $(STDLIB)/time/date/arithmetic.ha \ - $(STDLIB)/time/date/chronology.ha \ - $(STDLIB)/time/date/errors.ha \ $(STDLIB)/time/date/date.ha \ - $(STDLIB)/time/date/datetime.ha \ - $(STDLIB)/time/date/duration.ha \ + $(STDLIB)/time/date/daydate.ha \ + $(STDLIB)/time/date/daytime.ha \ + $(STDLIB)/time/date/error.ha \ $(STDLIB)/time/date/format.ha \ + $(STDLIB)/time/date/locality.ha \ + $(STDLIB)/time/date/observe.ha \ + $(STDLIB)/time/date/parithm.ha \ $(STDLIB)/time/date/parse.ha \ $(STDLIB)/time/date/period.ha \ $(STDLIB)/time/date/reckon.ha \ - $(STDLIB)/time/date/time.ha \ - $(STDLIB)/time/date/timezone.ha \ + $(STDLIB)/time/date/tarithm.ha \ $(STDLIB)/time/date/virtual.ha $(HARECACHE)/time/date/time_date-freebsd.ssa: $(stdlib_time_date_freebsd_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_time_chrono_$(PLATFORM)) @@ -4609,18 +4609,18 @@ $(TESTCACHE)/time/chrono/time_chrono-freebsd.ssa: $(testlib_time_chrono_freebsd_ # time::date (+linux) testlib_time_date_linux_srcs = \ - $(STDLIB)/time/date/arithmetic.ha \ - $(STDLIB)/time/date/chronology.ha \ - $(STDLIB)/time/date/errors.ha \ $(STDLIB)/time/date/date.ha \ - $(STDLIB)/time/date/datetime.ha \ - $(STDLIB)/time/date/duration.ha \ + $(STDLIB)/time/date/daydate.ha \ + $(STDLIB)/time/date/daytime.ha \ + $(STDLIB)/time/date/error.ha \ $(STDLIB)/time/date/format.ha \ + $(STDLIB)/time/date/locality.ha \ + $(STDLIB)/time/date/observe.ha \ + $(STDLIB)/time/date/parithm.ha \ $(STDLIB)/time/date/parse.ha \ $(STDLIB)/time/date/period.ha \ $(STDLIB)/time/date/reckon.ha \ - $(STDLIB)/time/date/time.ha \ - $(STDLIB)/time/date/timezone.ha \ + $(STDLIB)/time/date/tarithm.ha \ $(STDLIB)/time/date/virtual.ha $(TESTCACHE)/time/date/time_date-linux.ssa: $(testlib_time_date_linux_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM)) @@ -4631,18 +4631,18 @@ $(TESTCACHE)/time/date/time_date-linux.ssa: $(testlib_time_date_linux_srcs) $(te # time::date (+freebsd) testlib_time_date_freebsd_srcs = \ - $(STDLIB)/time/date/arithmetic.ha \ - $(STDLIB)/time/date/chronology.ha \ - $(STDLIB)/time/date/errors.ha \ $(STDLIB)/time/date/date.ha \ - $(STDLIB)/time/date/datetime.ha \ - $(STDLIB)/time/date/duration.ha \ + $(STDLIB)/time/date/daydate.ha \ + $(STDLIB)/time/date/daytime.ha \ + $(STDLIB)/time/date/error.ha \ $(STDLIB)/time/date/format.ha \ + $(STDLIB)/time/date/locality.ha \ + $(STDLIB)/time/date/observe.ha \ + $(STDLIB)/time/date/parithm.ha \ $(STDLIB)/time/date/parse.ha \ $(STDLIB)/time/date/period.ha \ $(STDLIB)/time/date/reckon.ha \ - $(STDLIB)/time/date/time.ha \ - $(STDLIB)/time/date/timezone.ha \ + $(STDLIB)/time/date/tarithm.ha \ $(STDLIB)/time/date/virtual.ha $(TESTCACHE)/time/date/time_date-freebsd.ssa: $(testlib_time_date_freebsd_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM)) diff --git a/time/date/date.ha b/time/date/date.ha @@ -1,598 +1,305 @@ // License: MPL-2.0 // (c) 2021-2022 Byron Torres <b@torresjrjr.com> -// (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net> +// (c) 2022 Drew DeVault <sir@cmpwn.com> use errors; +use time; use time::chrono; -// Hare internally uses the Unix epoch (1970-01-01) for calendrical logic. Here -// we provide useful constant for working with the astronomically numbered -// proleptic Gregorian calendar, as offsets from the Hare epoch. - -// The Hare epoch of the Julian Day Number. -export def EPOCHAL_JULIAN: i64 = -2440588; - -// The Hare epoch of the Gregorian Common Era. -export def EPOCHAL_GREGORIAN: i64 = -719164; - -// Calculates whether a year is a leap year. -export fn isleapyear(y: int) bool = { - return if (y % 4 != 0) false - else if (y % 100 != 0) true - else if (y % 400 != 0) false - else true; +// Invalid [[date]]. +export type invalid = !chrono::invalid; + +// A date/time object; a [[time::chrono::moment]] wrapper optimized for the +// Gregorian chronology, and by extension a [[time::instant]] wrapper. +// +// This object should be treated as private and immutable. Directly mutating its +// fields causes undefined behaviour when used with module functions. Likewise, +// interrogating the fields' type and value (e.g. using match statements) is +// also improper. +// +// A date observes various chronological values, cached in its fields. To +// evaluate and obtain these values, use the various observer functions +// ([[year]], [[hour]], etc.). These values are derived from the embedded moment +// information, and thus are guaranteed to be valid. +// +// See [[virtual]] for an public, mutable, intermediary representation of a +// date, which waives guarantees of validity. +export type date = struct { + chrono::moment, + + era: (void | int), + year: (void | int), + month: (void | int), + day: (void | int), + yearday: (void | int), + isoweekyear: (void | int), + isoweek: (void | int), + week: (void | int), + sundayweek: (void | int), + weekday: (void | int), + + hour: (void | int), + minute: (void | int), + second: (void | int), + nanosecond: (void | int), }; -// Calculates whether a given year, month, and day-of-month, is a valid date. -fn is_valid_ymd(y: int, m: int, d: int) bool = { - return m >= 1 && m <= 12 && d >= 1 && - d <= calc_month_daycnt(y, m); +fn init() date = date { + sec = 0, + nsec = 0, + loc = chrono::UTC, + zone = null, + daydate = void, + daytime = void, + + era = void, + year = void, + month = void, + day = void, + yearday = void, + isoweekyear = void, + isoweek = void, + week = void, + sundayweek = void, + weekday = void, + + hour = void, + minute = void, + second = void, + nanosecond = void, }; -// Calculates whether a given year, and day-of-year, is a valid date. -fn is_valid_yd(y: int, yd: int) bool = { - return yd >= 1 && yd <= calc_year_daycnt(y); +// Evaluates and populates all of a [[date]]'s fields. +fn all(d: *date) *date = { + _era(d); + _year(d); + _month(d); + _day(d); + _yearday(d); + _isoweekyear(d); + _isoweek(d); + _week(d); + _sundayweek(d); + _weekday(d); + + _hour(d); + _minute(d); + _second(d); + _nanosecond(d); + + return d; }; -// Calculates the number of days in the given month of the given year. -fn calc_month_daycnt(y: int, m: int) int = { - const days_per_month: [_]int = [ - 31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 +// Creates a new [[date]]. A maximum of 7 optional field arguments can be given: +// year, month, day-of-month, hour, minute, second, nanosecond. 8 or more causes +// an abort. +// +// // 0000-01-01 00:00:00.000000000 +0000 UTC UTC +// date::new(time::chrono::UTC, 0); +// +// // 2019-12-27 20:07:08.000031415 +0000 UTC UTC +// date::new(time::chrono::UTC, 0, 2019, 12, 27, 20, 07, 08, 31415); +// +// // 2019-12-27 21:00:00.000000000 +0100 CET Europe/Amsterdam +// date::new(time::chrono::tz("Europe/Amsterdam")!, 1 * time::HOUR, +// 2019, 12, 27, 21); +// +// 'zo' is the zone offset from the normal timezone (in most cases, UTC). For +// example, the "Asia/Tokyo" timezone has a single zoff of +9 hours, but the +// "Australia/Sydney" timezone has zoffs +10 hours and +11 hours, as they +// observe Daylight Saving Time. +// +// If specified (non-void), 'zo' must match one of the timezone's observed +// zoffs, or will fail. See [[time::chrono::fixedzone]] for custom timezones. +// +// You may omit the zoff. If the givem timezone has a single zone, [[new]] +// will use that zone's zoff. Otherwise [[new]] will try to infer the zoff +// from the multiple zones. This will fail during certain timezone transitions, +// where certain dates are ambiguous or nonexistent. For example: +// +// - In the Europe/Amsterdam timezone, at 1995 March 26th, +// the local time 02:30 was never observed, +// as the clock jumped forward 1 hour from 02:00 CET to 03:00 CEST. +// +// - In the Europe/Amsterdam timezone, at 1995 September 24th, +// the local time 02:30 was observed twice (00:30 UTC & 01:30 UTC), +// as the clock jumped back 1 hour from 03:00 CEST to 02:00 CET. +export fn new( + loc: chrono::locality, + zo: (time::duration | void), + fields: int... +) (date | invalid) = { + // TODO: + // - revise examples + // - Implement as described. + // - fix calls with `years <= -4715`. + // https://todo.sr.ht/~sircmpwn/hare/565 + let _fields: [_]int = [ + 0, 1, 1, // year month day + 0, 0, 0, 0, // hour min sec nsec ]; - if (m == 2) { - return if (isleapyear(y)) 29 else 28; - } else { - return days_per_month[m - 1]; - }; -}; - -// Calculates the number of days in a given year. -fn calc_year_daycnt(y: int) int = { - return if (isleapyear(y)) 366 else 365; -}; - -// Calculates the day-of-week of January 1st, given a year. -fn calc_janfirstweekday(y: int) int = { - const y = (y % 400) + 400; // keep year > 0 (using Gregorian cycle) - // Gauss' algorithm - const wd = ( - + 5 * ((y - 1) % 4) - + 4 * ((y - 1) % 100) - + 6 * ((y - 1) % 400) - ) % 7; - return wd; -}; -// Calculates the era, given a year. -fn calc_era(y: int) int = { - return if (y >= 0) { - yield 1; // CE "Common Era" - } else { - yield 0; // BCE "Before Common Era" + assert(len(fields) <= len(_fields), + "time::date::new(): Too many field arguments"); + _fields[..len(fields)] = fields; + + const year = _fields[0]; + const month = _fields[1]; + const day = _fields[2]; + const hour = _fields[3]; + const min = _fields[4]; + const sec = _fields[5]; + const nsec = _fields[6]; + + const mdate = calc_daydate__ymd(year, month, day)?; + const mtime = calc_daytime__hmsn(hour, min, sec, nsec)?; + + // create the moment + const m = match (zo) { + case let zo: time::duration => + yield chrono::from_datetime(loc, zo, mdate, mtime); + case void => + // TODO: Deduce the zone offset + // + // perform a zone lookup, then try that zone and the zones that + // are observed before and after. This requires knowlegde of the + // transition index. + abort("TODO: time::date::new(zo=void)"); }; -}; - -// Calculates the year, month, and day-of-month, given an epochal day. -fn calc_ymd(e: i64) (int, int, int) = { - // Algorithm adapted from: - // https://en.wikipedia.org/wiki/Julian_day#Julian_or_Gregorian_calendar_from_Julian_day_number - // - // Alternate methods of date calculation should be explored. - const J = e - EPOCHAL_JULIAN; - - // TODO: substitute numbers where possible - const b = 274277; - const c = -38; - const j = 1401; - const m = 2; - const n = 12; - const p = 1461; - const r = 4; - const s = 153; - const u = 5; - const v = 3; - const w = 2; - const y = 4716; - - const f = J + j + (((4 * J + b) / 146097) * 3) / 4 + c; - const a = r * f + v; - const g = (a % p) / r; - const h = u * g + w; - const D = (h % s) / u + 1; - const M = ((h / s + m) % n) + 1; - const Y = (a / p) - y + (n + m - M) / n; - - return (Y: int, M: int, D: int); -}; - -// Calculates the day-of-year, given a year, month, and day-of-month. -fn calc_yearday(y: int, m: int, d: int) int = { - const months_firsts: [_]int = [ - 0, 31, 59, - 90, 120, 151, - 181, 212, 243, - 273, 304, 334, - ]; + const d = from_moment(m); - if (m >= 3 && isleapyear(y)) { - return months_firsts[m - 1] + d + 1; - } else { - return months_firsts[m - 1] + d; + const zo = match (zo) { + case void => + yield chrono::mzone(&m).zoff; + case let zo: time::duration => + yield zo; }; -}; -// Calculates the ISO week-numbering year, -// given a year, month, day-of-month, and day-of-week. -fn calc_isoweekyear(y: int, m: int, d: int, wd: int) int = { + // check if input values are actually observed if ( - // if the date is within a week whose Thursday - // belongs to the previous Gregorian year - m == 1 && ( - (d == 1 && (wd == 4 || wd == 5 || wd == 6)) - || (d == 2 && (wd == 5 || wd == 6)) - || (d == 3 && wd == 6) - ) - ) { - return y - 1; - } else if ( - // if the date is within a week whose Thursday - // belongs to the next Gregorian year - m == 12 && ( - (d == 29 && wd == 0) - || (d == 30 && (wd == 0 || wd == 1)) - || (d == 31 && (wd == 0 || wd == 1 || wd == 2)) - ) + zo != chrono::mzone(&d).zoff + || year != _year(&d) + || month != _month(&d) + || day != _day(&d) + || hour != _hour(&d) + || min != _minute(&d) + || sec != _second(&d) + || nsec != _nanosecond(&d) ) { - return y + 1; - } else { - return y; - }; -}; - -// Calculates the ISO week, -// given a year, week, day-of-week, and day-of-year. -fn calc_isoweek(y: int, w: int) int = { - switch (calc_janfirstweekday(y)) { - case 0 => - return w; - case 1, 2, 3 => - return w + 1; - case 4 => - return if (w != 0) w else 53; - case 5 => - return if (w != 0) w else { - yield if (isleapyear(y - 1)) 53 else 52; - }; - case 6 => - return if (w != 0) w else 52; - case => - abort("Unreachable"); - }; -}; - -// Calculates the week within a Gregorian year [0..53], -// given a day-of-year and day-of-week. -// All days in a year before the year's first Monday belong to week 0. -fn calc_week(yd: int, wd: int) int = { - return (yd + 6 - wd) / 7; -}; - -// Calculates the week within a Gregorian year [0..53], -// given a day-of-year and day-of-week. -// All days in a year before the year's first Sunday belong to week 0. -fn calc_sundayweek(yd: int, wd: int) int = { - return (yd + 6 - ((wd + 1) % 7)) / 7; -}; - -// Calculates the day-of-week, given a epochal day, -// from Monday=0 to Sunday=6. -fn calc_weekday(e: i64) int = { - const wd = ((e + 3) % 7): int; - return (wd + 7) % 7; -}; - -// Calculates the daydate, -// given a year, month, and day-of-month. -fn calc_daydate__ymd(y: int, m: int, d: int) (i64 | invalid) = { - if (!is_valid_ymd(y, m, d)) { - return invalid; - }; - // Algorithm adapted from: - // https://en.wikipedia.org/wiki/Julian_day - // - // TODO: Review, cite, verify, annotate. - const jdn = ( - (1461 * (y + 4800 + (m - 14) / 12)) / 4 - + (367 * (m - 2 - 12 * ((m - 14) / 12))) / 12 - - (3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4 - + d - - 32075 - ); - const e = jdn + EPOCHAL_JULIAN; - return e; -}; - -// Calculates the daydate, -// given a year, week, and day-of-week. -fn calc_daydate__ywd(y: int, w: int, wd: int) (i64 | invalid) = { - const jan1wd = calc_janfirstweekday(y); - const yd = wd - jan1wd + 7 * w; - return calc_daydate__yd(y, yd)?; -}; - -// Calculates the daydate, -// given a year and day-of-year. -fn calc_daydate__yd(y: int, yd: int) (i64 | invalid) = { - if (yd < 1 || yd > calc_year_daycnt(y)) { return invalid; }; - return calc_daydate__ymd(y, 1, 1)? + yd - 1; -}; - -@test fn calc_daydate__ymd() void = { - const cases = [ - (( -768, 2, 5), -999999, false), - (( -1, 12, 31), -719529, false), - (( 0, 1, 1), -719528, false), - (( 0, 1, 2), -719527, false), - (( 0, 12, 31), -719163, false), - (( 1, 1, 1), -719162, false), - (( 1, 1, 2), -719161, false), - (( 1965, 3, 23), -1745, false), - (( 1969, 12, 31), -1, false), - (( 1970, 1, 1), 0, false), - (( 1970, 1, 2), 1, false), - (( 1999, 12, 31), 10956, false), - (( 2000, 1, 1), 10957, false), - (( 2000, 1, 2), 10958, false), - (( 2038, 1, 18), 24854, false), - (( 2038, 1, 19), 24855, false), - (( 2038, 1, 20), 24856, false), - (( 2243, 10, 17), 100000, false), - (( 4707, 11, 28), 999999, false), - (( 4707, 11, 29), 1000000, false), - ((29349, 1, 25), 9999999, false), - - (( 1970,-99,-99), 0, true), - (( 1970, -9, -9), 0, true), - (( 1970, -1, -1), 0, true), - (( 1970, 0, 0), 0, true), - (( 1970, 0, 1), 0, true), - (( 1970, 1, 99), 0, true), - (( 1970, 99, 99), 0, true), - ]; - for (let i = 0z; i < len(cases); i += 1) { - const params = cases[i].0; - const expect = cases[i].1; - const should_error = cases[i].2; - const actual = calc_daydate__ymd( - params.0, params.1, params.2, - ); - if (should_error) { - assert(actual is invalid, "invalid date accepted"); - } else { - assert(actual is i64, "valid date not accepted"); - assert(actual as i64 == expect, "date miscalculation"); - }; - }; + return d; }; -@test fn calc_daydate__ywd() void = { - const cases = [ - (( -768, 0, 4), -1000034), - (( -768, 5, 4), -999999), - (( -1, 52, 5), -719529), - (( 0, 0, 6), -719528), - (( 0, 0, 7), -719527), - (( 0, 52, 7), -719163), - (( 1, 0, 1), -719162), - (( 1, 0, 2), -719161), - (( 1965, 12, 2), -1745), - (( 1969, 52, 3), -1), - (( 1970, 0, 4), 0), - (( 1970, 0, 5), 1), - (( 1999, 52, 5), 10956), - (( 2000, 0, 6), 10957), - (( 2000, 0, 7), 10958), - (( 2020, 0, 3), 18262), - (( 2022, 9, 1), 19051), - (( 2022, 9, 2), 19052), - (( 2023, 51, 7), 19715), - (( 2024, 8, 3), 19781), - (( 2024, 8, 4), 19782), - (( 2024, 8, 5), 19783), - (( 2024, 49, 4), 20069), - (( 2024, 52, 2), 20088), - (( 2038, 3, 1), 24854), - (( 2038, 3, 2), 24855), - (( 2038, 3, 3), 24856), - (( 2243, 41, 2), 99993), - (( 4707, 47, 4), 999999), - (( 4707, 47, 5), 1000000), - ((29349, 3, 6), 9999999), - ]; - - for (let i = 0z; i < len(cases); i += 1) { - const ywd = cases[i].0; - const expected = cases[i].1; - const actual = calc_daydate__ywd(ywd.0, ywd.1, ywd.2)!; - assert(actual == expected, - "incorrect calc_daydate__ywd() result"); - }; +// Returns a [[date]] of the current system time using +// [[time::clock::REALTIME]], in the [[time::chrono::LOCAL]] locality. +export fn now() date = { + return from_instant(chrono::LOCAL, time::now(time::clock::REALTIME)); }; -@test fn calc_daydate__yd() void = { - const cases = [ - ( -768, 36, -999999), - ( -1, 365, -719529), - ( 0, 1, -719528), - ( 0, 2, -719527), - ( 0, 366, -719163), - ( 1, 1, -719162), - ( 1, 2, -719161), - ( 1965, 82, -1745 ), - ( 1969, 365, -1 ), - ( 1970, 1, 0 ), - ( 1970, 2, 1 ), - ( 1999, 365, 10956 ), - ( 2000, 1, 10957 ), - ( 2000, 2, 10958 ), - ( 2038, 18, 24854 ), - ( 2038, 19, 24855 ), - ( 2038, 20, 24856 ), - ( 2243, 290, 100000 ), - ( 4707, 332, 999999 ), - ( 4707, 333, 1000000), - (29349, 25, 9999999), - ]; - - for (let i = 0z; i < len(cases); i += 1) { - const y = cases[i].0; - const yd = cases[i].1; - const expected = cases[i].2; - const actual = calc_daydate__yd(y, yd)!; - assert(expected == actual, - "error in date calculation from yd"); - }; - assert(calc_daydate__yd(2020, 0) is invalid, - "calc_daydate__yd() did not reject invalid yearday"); - assert(calc_daydate__yd(2020, 400) is invalid, - "calc_daydate__yd() did not reject invalid yearday"); +// Returns a [[date]] of the current system time using +// [[time::clock::REALTIME]], in the [[time::chrono::UTC]] locality. +export fn nowutc() date = { + return from_instant(chrono::UTC, time::now(time::clock::REALTIME)); }; -@test fn calc_ymd() void = { - const cases = [ - (-999999, ( -768, 2, 5)), - (-719529, ( -1, 12, 31)), - (-719528, ( 0, 1, 1)), - (-719527, ( 0, 1, 2)), - (-719163, ( 0, 12, 31)), - (-719162, ( 1, 1, 1)), - (-719161, ( 1, 1, 2)), - ( -1745, ( 1965, 3, 23)), - ( -1, ( 1969, 12, 31)), - ( 0, ( 1970, 1, 1)), - ( 1, ( 1970, 1, 2)), - ( 10956, ( 1999, 12, 31)), - ( 10957, ( 2000, 1, 1)), - ( 10958, ( 2000, 1, 2)), - ( 24854, ( 2038, 1, 18)), - ( 24855, ( 2038, 1, 19)), - ( 24856, ( 2038, 1, 20)), - ( 100000, ( 2243, 10, 17)), - ( 999999, ( 4707, 11, 28)), - (1000000, ( 4707, 11, 29)), - (9999999, (29349, 1, 25)), - ]; - for (let i = 0z; i < len(cases); i += 1) { - const paramt = cases[i].0; - const expect = cases[i].1; - const actual = calc_ymd(paramt); - assert(expect.0 == actual.0, "year mismatch"); - assert(expect.1 == actual.1, "month mismatch"); - assert(expect.2 == actual.2, "day mismatch"); - }; -}; - -@test fn calc_yearday() void = { - const cases = [ - (( -768, 2, 5), 36), - (( -1, 12, 31), 365), - (( 0, 1, 1), 1), - (( 0, 1, 2), 2), - (( 0, 12, 31), 366), - (( 1, 1, 1), 1), - (( 1, 1, 2), 2), - (( 1965, 3, 23), 82), - (( 1969, 12, 31), 365), - (( 1970, 1, 1), 1), - (( 1970, 1, 2), 2), - (( 1999, 12, 31), 365), - (( 2000, 1, 1), 1), - (( 2000, 1, 2), 2), - (( 2020, 2, 12), 43), - (( 2038, 1, 18), 18), - (( 2038, 1, 19), 19), - (( 2038, 1, 20), 20), - (( 2243, 10, 17), 290), - (( 4707, 11, 28), 332), - (( 4707, 11, 29), 333), - ((29349, 1, 25), 25), - ]; - for (let i = 0z; i < len(cases); i += 1) { - const params = cases[i].0; - const expect = cases[i].1; - const actual = calc_yearday(params.0, params.1, params.2); - assert(expect == actual, "yearday miscalculation"); - }; +// Creates a [[date]] from a [[time::chrono::moment]]. +export fn from_moment(m: chrono::moment) date = { + const d = init(); + d.loc = m.loc; + d.sec = m.sec; + d.nsec = m.nsec; + d.daydate = m.daydate; + d.daytime = m.daytime; + d.zone = m.zone; + return d; }; -@test fn calc_week() void = { - const cases = [ - (( 1, 0), 1), - (( 1, 1), 0), - (( 1, 2), 0), - (( 1, 3), 0), - (( 1, 4), 0), - (( 1, 5), 0), - (( 1, 6), 0), - (( 21, 1), 3), - (( 61, 2), 9), - ((193, 4), 27), - ((229, 0), 33), - ((286, 3), 41), - ((341, 6), 48), - ((365, 5), 52), - ((366, 0), 53), - ]; - - for (let i = 0z; i < len(cases); i += 1) { - const params = cases[i].0; - const expect = cases[i].1; - const actual = calc_week(params.0, params.1); - assert(expect == actual, "week miscalculation"); - }; +// Creates a [[date]] from a [[time::instant]] +// in a [[time::chrono::locality]]. +export fn from_instant(loc: chrono::locality, i: time::instant) date = { + return from_moment(chrono::new(loc, i)); }; -@test fn calc_sundayweek() void = { - const cases = [ - (( 1, 0), 0), - (( 1, 1), 0), - (( 1, 2), 0), - (( 1, 3), 0), - (( 1, 4), 0), - (( 1, 5), 0), - (( 1, 6), 1), - (( 21, 1), 3), - (( 61, 2), 9), - ((193, 4), 27), - ((229, 0), 33), - ((286, 3), 41), - ((341, 6), 49), - ((365, 5), 52), - ((366, 0), 53), - ]; - - for (let i = 0z; i < len(cases); i += 1) { - const params = cases[i].0; - const expect = cases[i].1; - const actual = calc_sundayweek(params.0, params.1); - assert(expect == actual, "week miscalculation"); - }; +// Creates a [[date]] from a string, parsed according to a layout format. +// See [[parse]] and [[format]]. At least a complete calendar date has to be +// provided. The if hour, minute, second, nanosecond, or zone offset are not +// provided, they default to 0. +// +// let new = date::from_str( +// date::STAMP_NOZL, +// "2019-12-27 22:07:08.000000000 +0100 CET Europe/Amsterdam", +// locs... +// )!; +// +// The date's [[time::chrono::locality]] will be selected from the provided +// locality arguments. The 'name' field of these localities will be matched +// against the parsed result for the %L specifier. If %L is not specified, or if +// no locality is provided, [[time::chrono::UTC]] is used. +export fn from_str( + layout: str, + s: str, + locs: time::chrono::locality... +) (date | parsefail | insufficient | invalid) = { + const v = newvirtual(); + v.zoff = 0; + v.hour = 0; + v.minute = 0; + v.second = 0; + v.nanosecond = 0; + parse(&v, layout, s)?; + return realize(v, locs...)?; }; -@test fn calc_weekday() void = { - const cases = [ - (-999999, 3), // -0768-02-05 - (-719529, 4), // -0001-12-31 - (-719528, 5), // 0000-01-01 - (-719527, 6), // 0000-01-02 - (-719163, 6), // 0000-12-31 - (-719162, 0), // 0001-01-01 - (-719161, 1), // 0001-01-02 - ( -1745, 1), // 1965-03-23 - ( -1, 2), // 1969-12-31 - ( 0, 3), // 1970-01-01 - ( 1, 4), // 1970-01-02 - ( 10956, 4), // 1999-12-31 - ( 10957, 5), // 2000-01-01 - ( 10958, 6), // 2000-01-02 - ( 24854, 0), // 2038-01-18 - ( 24855, 1), // 2038-01-19 - ( 24856, 2), // 2038-01-20 - ( 100000, 1), // 2243-10-17 - ( 999999, 3), // 4707-11-28 - (1000000, 4), // 4707-11-29 - (9999999, 5), // 29349-01-25 +@test fn from_str() void = { + const amst = chrono::tz("Europe/Amsterdam")!; + defer chrono::timezone_free(amst); + + let testcases: [_](str, str, []chrono::locality, (date | error)) = [ + (STAMP_NOZL, "2001-02-03 15:16:17.123456789 +0000 UTC UTC", [], + new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17, 123456789)!), + (STAMP, "2001-02-03 15:16:17", [], + new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!), + (RFC3339, "2001-02-03T15:16:17+0000", [], + new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!), + ("%F", "2009-06-30", [], + new(chrono::UTC, 0, 2009, 6, 30)!), + ("%F %L", "2009-06-30 GPS", [chrono::TAI, chrono::GPS], + new(chrono::GPS, 0, 2009, 6, 30)!), + ("%F %T", "2009-06-30 01:02:03", [], + new(chrono::UTC, 0, 2009, 6, 30, 1, 2, 3)!), + ("%FT%T%Z", "2009-06-30T18:30:00Z", [], + new(chrono::UTC, 0, 2009, 6, 30, 18, 30)!), + ("%FT%T.%N%Z", "2009-06-30T18:30:00.987654321Z", [], + new(chrono::UTC, 0, 2009, 6, 30, 18, 30, 0, 987654321)!), + ("%FT%T%z %L", "2009-06-30T18:30:00+0200 Europe/Amsterdam", [amst], + new(amst, 2 * time::HOUR, 2009, 6, 30, 18, 30)!), + + ("%Y", "a", [], 'a': parsefail), + ("%X", "2008", [], '2': parsefail), ]; - for (let i = 0z; i < len(cases); i += 1) { - const paramt = cases[i].0; - const expect = cases[i].1; - const actual = calc_weekday(paramt); - assert(expect == actual, "weekday miscalculation"); - }; -}; -@test fn calc_janfirstweekday() void = { - const cases = [ - // year weekday - (1969, 2), - (1970, 3), - (1971, 4), - (1972, 5), - (1973, 0), - (1974, 1), - (1975, 2), - (1976, 3), - (1977, 5), - (1978, 6), - (1979, 0), - (1980, 1), - (1981, 3), - (1982, 4), - (1983, 5), - (1984, 6), - (1985, 1), - (1986, 2), - (1987, 3), - (1988, 4), - (1989, 6), - (1990, 0), - (1991, 1), - (1992, 2), - (1993, 4), - (1994, 5), - (1995, 6), - (1996, 0), - (1997, 2), - (1998, 3), - (1999, 4), - (2000, 5), - (2001, 0), - (2002, 1), - (2003, 2), - (2004, 3), - (2005, 5), - (2006, 6), - (2007, 0), - (2008, 1), - (2009, 3), - (2010, 4), - (2011, 5), - (2012, 6), - (2013, 1), - (2014, 2), - (2015, 3), - (2016, 4), - (2017, 6), - (2018, 0), - (2019, 1), - (2020, 2), - (2021, 4), - (2022, 5), - (2023, 6), - (2024, 0), - (2025, 2), - (2026, 3), - (2027, 4), - (2028, 5), - (2029, 0), - (2030, 1), - (2031, 2), - (2032, 3), - (2033, 5), - (2034, 6), - (2035, 0), - (2036, 1), - (2037, 3), - (2038, 4), - (2039, 5), - ]; - for (let i = 0z; i < len(cases); i += 1) { - const paramt = cases[i].0; - const expect = cases[i].1; - const actual = calc_janfirstweekday(paramt); - assert(expect == actual, "calc_janfirstweekday() miscalculation"); + let buf: [64]u8 = [0...]; + for (let i = 0z; i < len(testcases); i += 1) { + const t = testcases[i]; + const expect = t.3; + const actual = from_str(t.0, t.1, t.2...); + + match (expect) { + case let e: date => + assert(actual is date, "wanted 'date', got 'error'"); + assert(chrono::eq(&(actual as date), &e)!, + "incorrect 'date' value"); + case let e: parsefail => + assert(actual is parsefail, + "wanted 'parsefail', got other"); + case insufficient => + assert(actual is insufficient, + "wanted 'insufficient', got other"); + case invalid => + assert(actual is invalid, + "wanted 'invalid', got other"); + }; }; }; diff --git a/time/date/datetime.ha b/time/date/datetime.ha @@ -1,305 +0,0 @@ -// License: MPL-2.0 -// (c) 2021-2022 Byron Torres <b@torresjrjr.com> -// (c) 2022 Drew DeVault <sir@cmpwn.com> -use errors; -use time; -use time::chrono; - -// Invalid [[date]]. -export type invalid = !chrono::invalid; - -// A date/time object; a [[time::chrono::moment]] wrapper optimized for the -// Gregorian chronology, and by extension a [[time::instant]] wrapper. -// -// This object should be treated as private and immutable. Directly mutating its -// fields causes undefined behaviour when used with module functions. Likewise, -// interrogating the fields' type and value (e.g. using match statements) is -// also improper. -// -// A date observes various chronological values, cached in its fields. To -// evaluate and obtain these values, use the various observer functions -// ([[year]], [[hour]], etc.). These values are derived from the embedded moment -// information, and thus are guaranteed to be valid. -// -// See [[virtual]] for an public, mutable, intermediary representation of a -// date, which waives guarantees of validity. -export type date = struct { - chrono::moment, - - era: (void | int), - year: (void | int), - month: (void | int), - day: (void | int), - yearday: (void | int), - isoweekyear: (void | int), - isoweek: (void | int), - week: (void | int), - sundayweek: (void | int), - weekday: (void | int), - - hour: (void | int), - minute: (void | int), - second: (void | int), - nanosecond: (void | int), -}; - -fn init() date = date { - sec = 0, - nsec = 0, - loc = chrono::UTC, - zone = null, - daydate = void, - daytime = void, - - era = void, - year = void, - month = void, - day = void, - yearday = void, - isoweekyear = void, - isoweek = void, - week = void, - sundayweek = void, - weekday = void, - - hour = void, - minute = void, - second = void, - nanosecond = void, -}; - -// Evaluates and populates all of a [[date]]'s fields. -fn all(d: *date) *date = { - _era(d); - _year(d); - _month(d); - _day(d); - _yearday(d); - _isoweekyear(d); - _isoweek(d); - _week(d); - _sundayweek(d); - _weekday(d); - - _hour(d); - _minute(d); - _second(d); - _nanosecond(d); - - return d; -}; - -// Creates a new [[date]]. A maximum of 7 optional field arguments can be given: -// year, month, day-of-month, hour, minute, second, nanosecond. 8 or more causes -// an abort. -// -// // 0000-01-01 00:00:00.000000000 +0000 UTC UTC -// date::new(time::chrono::UTC, 0); -// -// // 2019-12-27 20:07:08.000031415 +0000 UTC UTC -// date::new(time::chrono::UTC, 0, 2019, 12, 27, 20, 07, 08, 31415); -// -// // 2019-12-27 21:00:00.000000000 +0100 CET Europe/Amsterdam -// date::new(time::chrono::tz("Europe/Amsterdam")!, 1 * time::HOUR, -// 2019, 12, 27, 21); -// -// 'zo' is the zone offset from the normal timezone (in most cases, UTC). For -// example, the "Asia/Tokyo" timezone has a single zoff of +9 hours, but the -// "Australia/Sydney" timezone has zoffs +10 hours and +11 hours, as they -// observe Daylight Saving Time. -// -// If specified (non-void), 'zo' must match one of the timezone's observed -// zoffs, or will fail. See [[time::chrono::fixedzone]] for custom timezones. -// -// You may omit the zoff. If the givem timezone has a single zone, [[new]] -// will use that zone's zoff. Otherwise [[new]] will try to infer the zoff -// from the multiple zones. This will fail during certain timezone transitions, -// where certain dates are ambiguous or nonexistent. For example: -// -// - In the Europe/Amsterdam timezone, at 1995 March 26th, -// the local time 02:30 was never observed, -// as the clock jumped forward 1 hour from 02:00 CET to 03:00 CEST. -// -// - In the Europe/Amsterdam timezone, at 1995 September 24th, -// the local time 02:30 was observed twice (00:30 UTC & 01:30 UTC), -// as the clock jumped back 1 hour from 03:00 CEST to 02:00 CET. -export fn new( - loc: chrono::locality, - zo: (time::duration | void), - fields: int... -) (date | invalid) = { - // TODO: - // - revise examples - // - Implement as described. - // - fix calls with `years <= -4715`. - // https://todo.sr.ht/~sircmpwn/hare/565 - let _fields: [_]int = [ - 0, 1, 1, // year month day - 0, 0, 0, 0, // hour min sec nsec - ]; - - assert(len(fields) <= len(_fields), - "time::date::new(): Too many field arguments"); - _fields[..len(fields)] = fields; - - const year = _fields[0]; - const month = _fields[1]; - const day = _fields[2]; - const hour = _fields[3]; - const min = _fields[4]; - const sec = _fields[5]; - const nsec = _fields[6]; - - const mdate = calc_daydate__ymd(year, month, day)?; - const mtime = calc_daytime__hmsn(hour, min, sec, nsec)?; - - // create the moment - const m = match (zo) { - case let zo: time::duration => - yield chrono::from_datetime(loc, zo, mdate, mtime); - case void => - // TODO: Deduce the zone offset - // - // perform a zone lookup, then try that zone and the zones that - // are observed before and after. This requires knowlegde of the - // transition index. - abort("TODO: time::date::new(zo=void)"); - }; - - const d = from_moment(m); - - const zo = match (zo) { - case void => - yield chrono::mzone(&m).zoff; - case let zo: time::duration => - yield zo; - }; - - // check if input values are actually observed - if ( - zo != chrono::mzone(&d).zoff - || year != _year(&d) - || month != _month(&d) - || day != _day(&d) - || hour != _hour(&d) - || min != _minute(&d) - || sec != _second(&d) - || nsec != _nanosecond(&d) - ) { - return invalid; - }; - - return d; -}; - -// Returns a [[date]] of the current system time using -// [[time::clock::REALTIME]], in the [[time::chrono::LOCAL]] locality. -export fn now() date = { - return from_instant(chrono::LOCAL, time::now(time::clock::REALTIME)); -}; - -// Returns a [[date]] of the current system time using -// [[time::clock::REALTIME]], in the [[time::chrono::UTC]] locality. -export fn nowutc() date = { - return from_instant(chrono::UTC, time::now(time::clock::REALTIME)); -}; - -// Creates a [[date]] from a [[time::chrono::moment]]. -export fn from_moment(m: chrono::moment) date = { - const d = init(); - d.loc = m.loc; - d.sec = m.sec; - d.nsec = m.nsec; - d.daydate = m.daydate; - d.daytime = m.daytime; - d.zone = m.zone; - return d; -}; - -// Creates a [[date]] from a [[time::instant]] -// in a [[time::chrono::locality]]. -export fn from_instant(loc: chrono::locality, i: time::instant) date = { - return from_moment(chrono::new(loc, i)); -}; - -// Creates a [[date]] from a string, parsed according to a layout format. -// See [[parse]] and [[format]]. At least a complete calendar date has to be -// provided. The if hour, minute, second, nanosecond, or zone offset are not -// provided, they default to 0. -// -// let new = date::from_str( -// date::STAMP_NOZL, -// "2019-12-27 22:07:08.000000000 +0100 CET Europe/Amsterdam", -// locs... -// )!; -// -// The date's [[time::chrono::locality]] will be selected from the provided -// locality arguments. The 'name' field of these localities will be matched -// against the parsed result for the %L specifier. If %L is not specified, or if -// no locality is provided, [[time::chrono::UTC]] is used. -export fn from_str( - layout: str, - s: str, - locs: time::chrono::locality... -) (date | parsefail | insufficient | invalid) = { - const v = newvirtual(); - v.zoff = 0; - v.hour = 0; - v.minute = 0; - v.second = 0; - v.nanosecond = 0; - parse(&v, layout, s)?; - return realize(v, locs...)?; -}; - -@test fn from_str() void = { - const amst = chrono::tz("Europe/Amsterdam")!; - defer chrono::timezone_free(amst); - - let testcases: [_](str, str, []chrono::locality, (date | error)) = [ - (STAMP_NOZL, "2001-02-03 15:16:17.123456789 +0000 UTC UTC", [], - new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17, 123456789)!), - (STAMP, "2001-02-03 15:16:17", [], - new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!), - (RFC3339, "2001-02-03T15:16:17+0000", [], - new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!), - ("%F", "2009-06-30", [], - new(chrono::UTC, 0, 2009, 6, 30)!), - ("%F %L", "2009-06-30 GPS", [chrono::TAI, chrono::GPS], - new(chrono::GPS, 0, 2009, 6, 30)!), - ("%F %T", "2009-06-30 01:02:03", [], - new(chrono::UTC, 0, 2009, 6, 30, 1, 2, 3)!), - ("%FT%T%Z", "2009-06-30T18:30:00Z", [], - new(chrono::UTC, 0, 2009, 6, 30, 18, 30)!), - ("%FT%T.%N%Z", "2009-06-30T18:30:00.987654321Z", [], - new(chrono::UTC, 0, 2009, 6, 30, 18, 30, 0, 987654321)!), - ("%FT%T%z %L", "2009-06-30T18:30:00+0200 Europe/Amsterdam", [amst], - new(amst, 2 * time::HOUR, 2009, 6, 30, 18, 30)!), - - ("%Y", "a", [], 'a': parsefail), - ("%X", "2008", [], '2': parsefail), - ]; - - let buf: [64]u8 = [0...]; - for (let i = 0z; i < len(testcases); i += 1) { - const t = testcases[i]; - const expect = t.3; - const actual = from_str(t.0, t.1, t.2...); - - match (expect) { - case let e: date => - assert(actual is date, "wanted 'date', got 'error'"); - assert(chrono::eq(&(actual as date), &e)!, - "incorrect 'date' value"); - case let e: parsefail => - assert(actual is parsefail, - "wanted 'parsefail', got other"); - case insufficient => - assert(actual is insufficient, - "wanted 'insufficient', got other"); - case invalid => - assert(actual is invalid, - "wanted 'invalid', got other"); - }; - }; -}; diff --git a/time/date/date.ha b/time/date/daydate.ha diff --git a/time/date/time.ha b/time/date/daytime.ha diff --git a/time/date/errors.ha b/time/date/error.ha diff --git a/time/date/timezone.ha b/time/date/locality.ha diff --git a/time/date/chronology.ha b/time/date/observe.ha diff --git a/time/date/arithmetic.ha b/time/date/parithm.ha diff --git a/time/date/duration.ha b/time/date/tarithm.ha