hare

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

commit 78cfe2883ee44bc3836528f1daa9453ce94baba6
parent 623c7eebf41c9d50aae67d5afc14530d56f60c78
Author: Byron Torres <b@torresjrjr.com>
Date:   Tue,  2 Aug 2022 01:13:28 +0100

time,time::chrono,datetime: overhaul

Herein lies significant changes to the date/time modules, along with
various bug fixes, tests, docs, and other auxiliary, minor improvements.

THE TIME MODULE

    The following are removed:

    * type ambiguous = ![]instant;
    * type nonexistent = !void;
    * type error = !(ambiguous | nonexistent);

    Their purpose was to provide the semantics for timescale conversions
    in the [[time::chrono]] module. They are replaced with new types
    which live there instead.

THE TIME::CHRONO MODULE

    The [[timescale]] interface is rehauled. New types replace the
    [[time::error]] types, though they now serve different meanings.

    * type analytical = ![]time::instant;
    * type discontinuity = !void;

    The [[timescale]] interface now uses the newly formed
    [[tsconverter]] type. [[ts_converter]] is removed. A new, handy
    [[convert]] function handles user called conversions neatly with
    error handling, avoiding the awkward manual two-step
    to_tai()/from_tai() process.

    The [[moment]].zone field's type has changed to improve memory
    usage.

    * Was:   zone: (void | zone),
    * Now:   zone: nullable *zone,

    The redundant [[lookupzone]] was removed. The following are renamed:

    * [[getdate]] -> [[date]]
    * [[gettime]] -> [[time]]
    * [[getzone]] -> [[mzone]]

    The previous [[date]] type (the i64 alias) was removed.

    New [[compare]], [[eq]], [[add]], and [[diff]] functions are added.
    They are designed to be interoperable with [[datetime::datetime]]
    and other such temporal types.

    New [[timezone_free]] and [[zone_finish]] functions are added.

THE DATETIME MODULE

Virtuals

    A new, more generic "virtual/real" analogy is adopted for
    intermediary datetime representations. This is akin to similar
    naming schemes found in other languages and libraries, such as
    "aware/naive", "floating/zoned", "mutable/immutable", etc.

    The following have been renamed:

    * [[builder]] -> [[virtual]]
    * [[newbuilder]] -> [[newvirtual]]
    * [[finish]] -> [[realize]]

    The virtual interface is now a struct which embeds the [[datetime]]
    type. Its new fields are used appropriately throughout the module
    (namely in [[realize]], [[parse]], [[reckon]]).

    The [[strategy]] enum was removed in favour of a simpler,
    predictable algorithm, documented in [[realize]].

    [[realize]] now handles localities and zone offsets, and has a new
    variadic localities parameter, which solves the problem of
    troublesome [[timezone]] allocations.

Periods

    The new [[sum]], [[neg]], [[abs]] functions handle vector math
    operations on [[period]]. [[period_eq]] is renamed to [[peq]].

    A [[period]]'s fields are now of type i64 instead of int.

Arithmetic

    Datetime arithmetic is categorized into timescale-wise (using
    [[time::duration]]) and chronology-wise (using [[period]]).

    The [[add]] function is transformed into a timescale-wise arithmetic
    operation and accepts [[time::duration]].

    The new [[reckon]] function provides a generic chronology-wise
    arithmetic operation, coupled with the now expanded [[calculus]]
    enum, which contains the new fields DEFAULT, REVSIG, FLOOR, CEIL,
    HOP, and FOLD.

    The "reckon" verb is used to disassociate the simple "linear math"
    expectations that the "add" verb brings, from what is essentially a
    unique complex modulo vector math. The analogy of a reckoner,
    reckoning through a chronology, like a journey, is used.

    The "add" verb is reserved for [[time::duration]] by convention, by
    the [[time]] and [[time::chrono]] modules, and Hare expects
    third-party datetime libraries to adopt the same convention.

    The [[hop]] function is removed. It does not prove useful enough
    with the advent of [[reckon]] and [[truncate]].

    The chronology-wise [[diff]] function has been renamed to [[pdiff]].
    The [[time::chrono::diff]] function should be used for a
    timescale-wise operation.

Parsing & formatting

    The parsing & formatting code is greatly improved, with many bug
    fixes.

    The new, rudimentary [[parsefail]] error type is added, and returned
    by [[parse]]. It previously returned [[invalid]], which was
    improper.

    The [[from_str]] function now accepts a variadic localities
    parameter, in accordance with the change to [[realize]].

Miscellaneous

    The [[epochal]] function is removed in favour of:

        time::chrono::date(&dt) - datetime::EPOCHAL_GERGORIAN;
        time::chrono::date(&dt) - datetime::EPOCHAL_JULIAN;

    The [[epochunix]] function removed in favour of:

        time::unix(*(&dt: *time::instant));
        dt.sec;

    [[is_leap_year]] is renamed to [[isleapyear]].

    The new [[STAMP_ZOFF]] layout is added.

    The new [[nowutc]] function accompanies [[now]].

    The [[weekday]] function now returns the range Monday=0 to Sunday=6.

    The new [[error]] type and [[strerror]] function are added.

Diffstat:
Mdatetime/README | 46+++++++++++++++++++++++++---------------------
Mdatetime/arithmetic.ha | 744+++++++++++++++++--------------------------------------------------------------
Mdatetime/chronology.ha | 35+++++++++--------------------------
Mdatetime/date.ha | 394+++++++++++++++++++++++++++++++++++++------------------------------------------
Mdatetime/datetime.ha | 263+++++++++++++++++++++++++++++++++++--------------------------------------------
Adatetime/duration.ha | 11+++++++++++
Adatetime/errors.ha | 18++++++++++++++++++
Mdatetime/format.ha | 233+++++++++++++------------------------------------------------------------------
Mdatetime/parse.ha | 506+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Adatetime/period.ha | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adatetime/reckon.ha | 489+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdatetime/time.ha | 2+-
Mdatetime/timezone.ha | 6++++--
Adatetime/virtual.ha | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/gen-stdlib | 14++++++++++++--
Mstdlib.mk | 28++++++++++++++++++++++++----
Mtime/chrono/README | 29++++++++++++++++++++---------
Atime/chrono/arithmetic.ha | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtime/chrono/chronology.ha | 101++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mtime/chrono/error.ha | 12+++++++++++-
Mtime/chrono/leapsec.ha | 5+++--
Mtime/chrono/timescale.ha | 303+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mtime/chrono/timezone.ha | 142+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mtime/chrono/tzdb.ha | 40++++++++++++++++++++++------------------
Mtime/types.ha | 9---------
25 files changed, 2232 insertions(+), 1568 deletions(-)

diff --git a/datetime/README b/datetime/README @@ -4,28 +4,32 @@ based on the astronomically numbered proleptic Gregorian calendar, as per ISO of civil date/time and an extension of the [[time::chrono::moment]] type, optimized for dealing with the Gregorian chronology. -Datetimes are created with [[new]], [[now]], or with one of the various "from_" -functions. Alternatively, use a [[builder]] to construct a datetime -piece-by-piece, by field assignements or by parsing strings with [[parse]]. - -[[datetime]] instances are designed to be always valid and internally -consistent. They should be treated as immutable, and their fields as private. -All functions herein return valid datetimes (or appropriate errors), and never -modify a datetime's value, even if passed as a pointer, which is used only for -internal caching. - -[[datetime]] fields are accessed, evaluated, and cached via the various "field" -functions ([[year]], [[month]], [[day]], etc). Accessing or modifying a -[[datetime]]'s fields directly is discouraged. To mutate a datetime in code, the -use of the [[builder]] interface is recommended. - -[[datetime]]s may be localized to different [[time::chrono::timezone]]s via the -[[in]] function. The "field" functions will evaluate the correct values +The [[time::chrono]] module has many useful functions which interoperate with +datetimes. Any [[time::chrono]] function which accepts *moment also accepts +*datetime. + +Datetimes are created with [[new]], [[now]], or one of the "from_" functions. +Alternatively, use the [[virtual]] interface to construct a datetime. + +The [[virtual]] interface, coupled with the [[realize]] function, provides a way +to handle uncertain or invalid datetime information intermediately, transform +date/time values with arithmetic, [[parse]] date/time strings, and construct new +datetimes safely. + +The "observe" functions accept a *datetime and evaluates one of its observed +chronological values. This includes [[year]], [[month]], [[day]], [[hour]] etc. + +[[datetime]]s may be localized to different [[time::chrono::locality]]s via the +[[in]] function. The "observe" functions will evaluate the correct values accordingly. You'll find a standard selection of world timezones in the [[time::chrono]] module. -To convert datetimes to and from strings, use [[parse]] and [[format]]. +See [[parse]] and [[format]] for working with date/time strings. + +Timescale-wise datetime arithmetic using the [[time::duration]] type is possible, +with [[add]] and [[time::chrono::diff]]. -For arithmetics, use [[diff]], [[add]] and [[hop]]. Note that calendrical -arithmetic is highly irregular with many edge cases, so think carefully about -what you want. +Chronology-wise datetime arithmetic using the [[period]] type is possible, with +[[reckon]] and the [[calculus]] type, [[pdiff]], [[unitdiff]], and [[truncate]]. +Note that chronological and calendrical arithmetic is highly irregular due to +overflows and timezone discontinuities, so think carefully about what you want. diff --git a/datetime/arithmetic.ha b/datetime/arithmetic.ha @@ -7,35 +7,8 @@ use math; use time; use time::chrono; -// Represents a span of time in the Gregorian chronology, using nominal units of -// time. Used for datetime arithmetic. -export type period = struct { - eras: int, - years: int, - - // Can be 28, 29, 30, or 31 days long - months: int, - - // Weeks start on Monday - weeks: int, - - days: int, - hours: int, - minutes: int, - seconds: int, - nanoseconds: i64, -}; - -// Specifies the behaviour of calendar arithmetic. -export type calculus = enum int { - // Units are added in the order of largest (years) to smallest - // (nanoseconds). If the resulting date does not exist, the first extant - // date previous to the initial result is returned. - DEFAULT, -}; -// TODO: ^ Expand this - -// The nominal units of the Gregorian chronology. Used for datetime arithmetic. +// The nominal units of the Gregorian chronology. Used for chronological +// arithmetic. export type unit = enum int { ERA, YEAR, @@ -48,388 +21,164 @@ export type unit = enum int { NANOSECOND, }; -// Returns true if two [[datetime]]s are equivalent. -// -// Equivalence means they represent the same moment in time, regardless of their -// locality or observed chronological values. -export fn eq(a: datetime, b: datetime) bool = { - return time::compare(*(&a: *time::instant), *(&b: *time::instant)) == 0; -}; +// Calculates the [[period]] between two [[datetime]]s, from A to B. +// The returned period, provided to [[reckon]] along with A, will produce B, +// regardless of the [[calculus]] used. All the period's non-zero fields will +// have the same sign. +export fn pdiff(a: datetime, b: datetime) period = { + let p = period { ... }; -// Returns true if [[datetime]] "a" succeeds [[datetime]] "b". -// -// Temporal order is evaluated in a universal frame of reference, regardless of -// their locality or observed chronological values. -export fn after(a: datetime, b: datetime) bool = { - return time::compare(*(&a: *time::instant), *(&b: *time::instant)) == +1; -}; - -// Returns true if [[datetime]] "a" precedes [[datetime]] "b". -// -// Temporal order is evaluated in a universal frame of reference, regardless of -// their locality or observed chronological values. -export fn before(a: datetime, b: datetime) bool = { - return time::compare(*(&a: *time::instant), *(&b: *time::instant)) == -1; -}; - -// Calculates the [[period]] between two [[datetime]]s. -export fn diff(a: datetime, b: datetime) period = { - let res = period { ... }; - if (eq(a, b)) { - return res; + if (chrono::compare(&a, &b) == 0) { + return p; }; - if (after(b, a)) { - const tmp = a; + + let reverse = if (chrono::compare(&a, &b) > 0) true else false; + if (reverse) { + let tmp = a; a = b; b = tmp; }; - res.years = year(&a) - year(&b); + p.years = _year(&b) - _year(&a); - res.months = month(&a) - month(&b); - if (res.months < 0) { - res.years -= 1; - res.months = 12 + res.months; + p.months = _month(&b) - _month(&a); + if (p.months < 0) { + p.years -= 1; + p.months += 12; }; - res.days = day(&a) - day(&b); - if (res.days < 0) { - let prev_month_year = year(&a); - let prev_month = month(&a) - 1; - if (prev_month == 0) { - prev_month_year -= 1; - prev_month = 12; + p.days = _day(&b) - _day(&a); + let year = _year(&b); + let month = _month(&b); + let daycnt = calc_month_daycnt(year, month); + for (_day(&a) > daycnt || p.days < 0) { + month -= 1; + if (month == 0) { + year -= 1; + month = 12; }; - const n_days_in_prev_month = calc_n_days_in_month( - prev_month_year, prev_month); - res.months -= 1; - res.days = n_days_in_prev_month + res.days; + daycnt = calc_month_daycnt(year, month); + + p.months -= 1; + if (p.months < 0) { + p.years -= 1; + p.months += 12; + }; + p.days += daycnt; }; - res.hours = hour(&a) - hour(&b); - if (res.hours < 0) { - res.days -= 1; - res.hours = 24 + res.hours; + p.hours = _hour(&b) - _hour(&a); + if (p.hours < 0) { + p.days -= 1; + p.hours += 24; }; - res.minutes = minute(&a) - minute(&b); - if (res.minutes < 0) { - res.hours -= 1; - res.minutes = 60 + res.minutes; + p.minutes = _minute(&b) - _minute(&a); + if (p.minutes < 0) { + p.hours -= 1; + p.minutes += 60; }; - res.seconds = second(&a) - second(&b); - if (res.seconds < 0) { - res.minutes -= 1; - res.seconds = 60 + res.seconds; + p.seconds = _second(&b) - _second(&a); + if (p.seconds < 0) { + p.minutes -= 1; + p.seconds += 60; }; - res.nanoseconds = nanosecond(&a) - nanosecond(&b); - if (res.nanoseconds < 0) { - res.seconds -= 1; - res.nanoseconds = time::SECOND + res.nanoseconds; + p.nanoseconds = _nanosecond(&b) - _nanosecond(&a); + if (p.nanoseconds < 0) { + p.seconds -= 1; + p.nanoseconds += 1000000000; // 10E9 }; - return res; + return if (reverse) neg(p) else p; }; -// Calculates the difference between two [[datetime]]s using the given nominal -// [[unit]], truncating towards zero. +// Calculates the nominal [[unit]] difference between two [[datetime]]s. export fn unitdiff(a: datetime, b: datetime, u: unit) i64 = { - return switch (u) { + switch (u) { case unit::ERA => - yield math::absi(era(&a) - era(&b)): i64; + return era(&b) - era(&a); case unit::YEAR => - yield diff(a, b).years; + return pdiff(a, b).years; case unit::MONTH => - const full_diff = diff(a, b); - yield full_diff.years * 12 + full_diff.months; + const d = pdiff(a, b); + return d.years * 12 + d.months; case unit::WEEK => - yield unitdiff(a, b, unit::DAY) / 7; + return unitdiff(a, b, unit::DAY) / 7; case unit::DAY => - yield math::absi(chrono::getdate(&a) - chrono::getdate(&b)): int; + return chrono::date(&b) - chrono::date(&a); case unit::HOUR => - const full_diff = diff(a, b); - yield (unitdiff(a, b, unit::DAY) * 24) + full_diff.hours; + return unitdiff(a, b, unit::DAY) * 24 + pdiff(a, b).hours; case unit::MINUTE => - const full_diff = diff(a, b); - yield unitdiff(a, b, unit::HOUR) * 60 + full_diff.minutes; + return unitdiff(a, b, unit::HOUR) * 60 + pdiff(a, b).minutes; case unit::SECOND => - const full_diff = diff(a, b); - yield unitdiff(a, b, unit::MINUTE) * 60 + full_diff.seconds; + return unitdiff(a, b, unit::MINUTE) * 60 + pdiff(a, b).seconds; case unit::NANOSECOND => - const full_diff = diff(a, b); - yield unitdiff(a, b, unit::SECOND) * time::SECOND + - full_diff.nanoseconds; + return unitdiff(a, b, unit::SECOND) * 1000000000 + pdiff(a, b).nanoseconds; }; }; -// Returns true if two [[period]]s are numerically equal. -export fn period_eq(a: period, b: period) bool = { - return a.eras == b.eras && - a.years == b.years && - a.months == b.months && - a.weeks == b.weeks && - a.days == b.days && - a.hours == b.hours && - a.minutes == b.minutes && - a.seconds == b.seconds && - a.nanoseconds == b.nanoseconds; -}; - // Truncates the given [[datetime]] at the provided nominal [[unit]]. // -// For example, truncating to the nearest [[unit::MONTH]] will set the day, -// hour, minute, seconds, and nanoseconds fields to their minimum values. +// For example, truncating to the nearest unit::MONTH will set the 'day', +// 'hour', 'minute', 'second', and 'nanosecond' fields to their minimum values. export fn truncate(dt: datetime, u: unit) datetime = { - // TODO: Replace all of the 0s for the zoffset with the actual - // zoffset once the API is solidified a bit + // TODO: There exist timezones where midnight is invalid on certain + // days. The new()! calls will fail, but we probably don't want to '?' + // propagate [[invalid]] to keep this function's use simple. The minimum + // values (the zeroes and ones here) can't be hardcoded. They need + // calculation. We should either handle this here; or probably in + // realize(), and then use realize() here. return switch (u) { case unit::ERA => - yield new(dt.loc, 0, + yield new(dt.loc, chrono::mzone(&dt).zoff, 1, 1, 1, 0, 0, 0, 0, )!; case unit::YEAR => - yield new(dt.loc, 0, - year(&dt), 1, 1, + yield new(dt.loc, chrono::mzone(&dt).zoff, + _year(&dt), 1, 1, 0, 0, 0, 0, )!; case unit::MONTH => - yield new(dt.loc, 0, - year(&dt), month(&dt), 1, + yield new(dt.loc, chrono::mzone(&dt).zoff, + _year(&dt), _month(&dt), 1, 0, 0, 0, 0, )!; case unit::WEEK => - const date = chrono::getdate(&dt) - (weekday(&dt) - 1); + const date = chrono::date(&dt) - _weekday(&dt); const ymd = calc_ymd(date); - yield new(dt.loc, 0, + yield new(dt.loc, chrono::mzone(&dt).zoff, ymd.0, ymd.1, ymd.2, 0, 0, 0, 0, )!; case unit::DAY => - yield new(dt.loc, 0, - year(&dt), month(&dt), day(&dt), + yield new(dt.loc, chrono::mzone(&dt).zoff, + _year(&dt), _month(&dt), _day(&dt), 0, 0, 0, 0, )!; case unit::HOUR => - yield new(dt.loc, 0, - year(&dt), month(&dt), day(&dt), - hour(&dt), 0, 0, 0, + yield new(dt.loc, chrono::mzone(&dt).zoff, + _year(&dt), _month(&dt), _day(&dt), + _hour(&dt), 0, 0, 0, )!; case unit::MINUTE => - yield new(dt.loc, 0, - year(&dt), month(&dt), day(&dt), - hour(&dt), minute(&dt), 0, 0, + yield new(dt.loc, chrono::mzone(&dt).zoff, + _year(&dt), _month(&dt), _day(&dt), + _hour(&dt), _minute(&dt), 0, 0, )!; case unit::SECOND => - yield new(dt.loc, 0, - year(&dt), month(&dt), day(&dt), - hour(&dt), minute(&dt), second(&dt), 0, + yield new(dt.loc, chrono::mzone(&dt).zoff, + _year(&dt), _month(&dt), _day(&dt), + _hour(&dt), _minute(&dt), _second(&dt), 0, )!; case unit::NANOSECOND => yield dt; }; }; -// Given a [[datetime]] and a [[period]], "hops" to the minimum value of each -// field (years, months, days, etc) plus or minus an offset, and returns a new -// datetime. This can be used, for example, to find the start of last year. -// -// Consults each period's fields from most to least significant (from years to -// nanoseconds). -// -// If a period's field's value N is zero, it's a no-op. Otherwise, hop will -// reckon to the Nth inter-period point from where last reckoned. This repeats -// until all the given period's fields are exhausted. -// -// let dt = ... // 1999-05-13 12:30:45 -// datetime::hop(dt, datetime::period { -// years = 22, // produces 2021-01-01 00:00:00 -// months = -1, // produces 2020-11-01 00:00:00 -// days = -4, // produces 2020-10-27 00:00:00 -// }); -// -export fn hop(dt: datetime, pp: period...) datetime = { - let new_dt = dt; - for (let i = 0z; i < len(pp); i += 1) { - const p = pp[i]; - - if (p.years != 0) { - const dt_inc = add(new_dt, calculus::DEFAULT, - period { years = p.years, ... }); - new_dt = truncate(dt_inc, unit::YEAR); - }; - if (p.months != 0) { - const dt_inc = add(new_dt, calculus::DEFAULT, - period { months = p.months, ... }); - new_dt = truncate(dt_inc, unit::MONTH); - }; - if (p.weeks != 0) { - const dt_inc = add(new_dt, calculus::DEFAULT, - period { weeks = p.weeks, ... }); - new_dt = truncate(dt_inc, unit::WEEK); - }; - if (p.days != 0) { - const dt_inc = add(new_dt, calculus::DEFAULT, - period { days = p.days, ... }); - new_dt = truncate(dt_inc, unit::DAY); - }; - if (p.hours != 0) { - const dt_inc = add(new_dt, calculus::DEFAULT, - period { hours = p.hours, ... }); - new_dt = truncate(dt_inc, unit::HOUR); - }; - if (p.minutes != 0) { - const dt_inc = add(new_dt, calculus::DEFAULT, - period { minutes = p.minutes, ... }); - new_dt = truncate(dt_inc, unit::MINUTE); - }; - if (p.seconds != 0) { - const dt_inc = add(new_dt, calculus::DEFAULT, - period { seconds = p.seconds, ... }); - new_dt = truncate(dt_inc, unit::SECOND); - }; - if (p.nanoseconds != 0) { - new_dt = add(new_dt, calculus::DEFAULT, - period { nanoseconds = p.nanoseconds, ... }); - }; - }; - return new_dt; -}; - -// Adds a period of time to a datetime, most significant units first. Conserves -// relative distance from cyclical points on the calendar when possible. This -// can be used, for example, to find the date one year from now. -// -// let dt = ... // 1999-05-13 12:30:45 -// datetime::add(dt, datetime::calculus::DEFAULT, datetime::period { -// years = 22, // 2021-05-13 12:30:45 -// months = -1, // 2021-04-13 12:30:45 -// days = -4, // 2020-04-09 12:30:45 -// }); -// -export fn add(dt: datetime, flag: calculus, pp: period...) datetime = { - // TODO: Use [[builder]] to simplify some code. - let d_year = year(&dt); - let d_month = month(&dt); - let d_day = day(&dt); - let d_hour = hour(&dt); - let d_minute = minute(&dt); - let d_second = second(&dt); - let d_nanosecond = ((nanosecond(&dt)): i64); - for (let i = 0z; i < len(pp); i += 1) { - const p = pp[i]; - - let latest_date = chrono::getdate(&dt); - - if (p.years != 0) { - d_year += p.years; - }; - if (p.months != 0) { - d_month += p.months; - }; - if (d_month > 12) { - d_year += (d_month - 1) / 12; - d_month = d_month % 12; - }; - if (d_month < 1) { - d_year -= (12 + -(d_month - 1)) / 12; - d_month = 12 - (-d_month % 12); - }; - const n_days_in_month = calc_n_days_in_month(d_year, d_month); - if (d_day > n_days_in_month) { - d_day = n_days_in_month; - }; - - if (p.weeks != 0) { - p.days += p.weeks * 7; - }; - latest_date = calc_date_from_ymd(d_year, d_month, d_day)!; - if (p.days != 0) { - const new_ymd = calc_ymd(latest_date + p.days); - d_year = new_ymd.0; - d_month = new_ymd.1; - d_day = new_ymd.2; - latest_date = calc_date_from_ymd( - d_year, d_month, d_day)!; - }; - - if (p.hours != 0) { - p.nanoseconds += p.hours * time::HOUR; - }; - if (p.minutes != 0) { - p.nanoseconds += p.minutes * time::MINUTE; - }; - if (p.seconds != 0) { - p.nanoseconds += p.seconds * time::SECOND; - }; - if (p.nanoseconds != 0) { - const ns_in_day = 24 * time::HOUR; - let overflowed_days = 0; - - if (math::absi(p.nanoseconds): i64 > ns_in_day) { - overflowed_days += - ((p.nanoseconds / ns_in_day): int); - p.nanoseconds %= ns_in_day; - }; - - let new_time = chrono::gettime(&dt) + p.nanoseconds; - - if (new_time >= ns_in_day) { - overflowed_days += 1; - new_time -= ns_in_day; - } else if (new_time < 0) { - overflowed_days -= 1; - new_time += ns_in_day; - }; - - if (overflowed_days != 0) { - const new_date = latest_date + overflowed_days; - const new_ymd = calc_ymd(new_date); - d_year = new_ymd.0; - d_month = new_ymd.1; - d_day = new_ymd.2; - }; - const new_hmsn = calc_hmsn(new_time); - d_hour = new_hmsn.0; - d_minute = new_hmsn.1; - d_second = new_hmsn.2; - d_nanosecond = new_hmsn.3; - }; - }; - // TODO: Add zoffset back in here once API is settled - return new(dt.loc, 0, - d_year, d_month, d_day, d_hour, d_minute, d_second, d_nanosecond: int, - )!; -}; - -// Subtracts a calendrical period of time to a datetime, most significant units -// first. Conserves relative distance from cyclical points on the calendar when -// possible. -// -// let dt = ... // 1999-05-13 12:30:45 -// datetime::subtract(dt, datetime::calculus::DEFAULT, datetime::period { -// years = 22, // 1977-05-13 12:30:45 -// months = -1, // 1977-06-13 12:30:45 -// days = -4, // 1977-06-17 12:30:45 -// }); -// -export fn sub(dt: datetime, flag: calculus, pp: period...) datetime = { - for (let i = 0z; i < len(pp); i += 1) { - pp[i].eras *= -1; - pp[i].years *= -1; - pp[i].months *= -1; - pp[i].weeks *= -1; - pp[i].days *= -1; - pp[i].minutes *= -1; - pp[i].seconds *= -1; - pp[i].nanoseconds *= -1; - }; - return add(dt, flag, pp...); -}; - -@test fn diff() void = { +@test fn pdiff() void = { const cases = [ ( new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, @@ -540,33 +289,33 @@ export fn sub(dt: datetime, flag: calculus, pp: period...) datetime = { const dta = cases[i].0; const dtb = cases[i].1; const expected = cases[i].2; - const actual = diff(dta, dtb); - assert(period_eq(actual, expected), "diff miscalculation"); + const actual = pdiff(dta, dtb); + assert(peq(actual, expected), "pdiff miscalculation"); }; }; @test fn unitdiff() void = { const cases = [ ( - new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!, - new(chrono::UTC, 0, 2022, 1, 5, 13, 53, 30, 20)!, + new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!, + new(chrono::UTC, 0, 2022, 1, 5, 13, 53, 30, 20)!, (27, 328, 1427, 9993, 239834, 14390073, 863404409i64, (863404409i64 * time::SECOND) + 18), ), ( - new(chrono::UTC, 0, 1994, 8, 28, 11, 20, 1, 2)!, - new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, + new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, + new(chrono::UTC, 0, 1994, 8, 28, 11, 20, 1, 2)!, (0, 0, 0, 1, 24, 1440, 86400i64, (86400i64 * time::SECOND) + 2), ), ( - new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, - new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, + new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, + new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!, (0, 0, 0, 0, 0, 0, 0i64, 0i64), ), ( - new(chrono::UTC, 0, -500, 1, 1, 0, 59, 1, 0)!, - new(chrono::UTC, 0, 2000, 1, 1, 23, 1, 1, 0)!, + new(chrono::UTC, 0, -500, 1, 1, 0, 59, 1, 0)!, + new(chrono::UTC, 0, 2000, 1, 1, 23, 1, 1, 0)!, (2500, 30000, 130443, 913106, 913106 * 24 + 22, (913106 * 24 + 22) * 60 + 2, ((913106 * 24 + 22) * 60 + 2) * 60i64, @@ -578,255 +327,70 @@ export fn sub(dt: datetime, flag: calculus, pp: period...) datetime = { const dta = cases[i].0; const dtb = cases[i].1; const expected = cases[i].2; - assert(unitdiff(dtb, dta, unit::YEAR) == expected.0, + assert(unitdiff(dta, dtb, unit::YEAR) == expected.0, "invalid diff_in_years() result"); - assert(unitdiff(dtb, dta, unit::MONTH) == expected.1, + assert(unitdiff(dta, dtb, unit::MONTH) == expected.1, "invalid diff_in_months() result"); - assert(unitdiff(dtb, dta, unit::WEEK) == expected.2, + assert(unitdiff(dta, dtb, unit::WEEK) == expected.2, "invalid diff_in_weeks() result"); - assert(unitdiff(dtb, dta, unit::DAY) == expected.3, + assert(unitdiff(dta, dtb, unit::DAY) == expected.3, "invalid diff_in_days() result"); - assert(unitdiff(dtb, dta, unit::HOUR) == expected.4, + assert(unitdiff(dta, dtb, unit::HOUR) == expected.4, "invalid diff_in_hours() result"); - assert(unitdiff(dtb, dta, unit::MINUTE) == expected.5, + assert(unitdiff(dta, dtb, unit::MINUTE) == expected.5, "invalid diff_in_minutes() result"); - assert(unitdiff(dtb, dta, unit::SECOND) == expected.6, + assert(unitdiff(dta, dtb, unit::SECOND) == expected.6, "invalid diff_in_seconds() result"); - assert(unitdiff(dtb, dta, unit::NANOSECOND) == expected.7, + assert(unitdiff(dta, dtb, unit::NANOSECOND) == expected.7, "invalid diff_in_nanoseconds() result"); }; }; @test fn truncate() void = { const dt = new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!; - assert(eq(truncate(dt, unit::ERA), - new(chrono::UTC, 0, 1, 1, 1, 0, 0, 0, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::YEAR), - new(chrono::UTC, 0, 1994, 1, 1, 0, 0, 0, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::MONTH), - new(chrono::UTC, 0, 1994, 8, 1, 0, 0, 0, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::WEEK), - new(chrono::UTC, 0, 1994, 8, 22, 0, 0, 0, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::DAY), - new(chrono::UTC, 0, 1994, 8, 27, 0, 0, 0, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::HOUR), - new(chrono::UTC, 0, 1994, 8, 27, 11, 0, 0, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::MINUTE), - new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 0, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::SECOND), - new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!), - "invalid truncate() result"); - assert(eq(truncate(dt, unit::NANOSECOND), dt), - "invalid truncate() result"); -}; -@test fn add() void = { - const d = new(chrono::UTC, 0, 2022, 2, 4, 3, 14, 7, 0)!; - const cases = [ - ( - period { years = 1, ... }, - new(chrono::UTC, 0, 2023, 2, 4, 3, 14, 7, 0)!, - ), - ( - period { years = -23, ... }, - new(chrono::UTC, 0, 1999, 2, 4, 3, 14, 7, 0)!, - ), - ( - period { months = 2, ... }, - new(chrono::UTC, 0, 2022, 4, 4, 3, 14, 7, 0)!, - ), - ( - period { months = 11, ... }, - new(chrono::UTC, 0, 2023, 1, 4, 3, 14, 7, 0)!, - ), - ( - period { months = -1, ... }, - new(chrono::UTC, 0, 2022, 1, 4, 3, 14, 7, 0)!, - ), - ( - period { months = -2, ... }, - new(chrono::UTC, 0, 2021, 12, 4, 3, 14, 7, 0)!, - ), - ( - period { days = 3, ... }, - new(chrono::UTC, 0, 2022, 2, 7, 3, 14, 7, 0)!, - ), - ( - period { days = 33, ... }, - new(chrono::UTC, 0, 2022, 3, 9, 3, 14, 7, 0)!, - ), - ( - period { days = 333, ... }, - new(chrono::UTC, 0, 2023, 1, 3, 3, 14, 7, 0)!, - ), - ( - period { days = -2, ... }, - new(chrono::UTC, 0, 2022, 2, 2, 3, 14, 7, 0)!, - ), - ( - period { days = -4, ... }, - new(chrono::UTC, 0, 2022, 1, 31, 3, 14, 7, 0)!, - ), - ( - period { days = -1337, ... }, - new(chrono::UTC, 0, 2018, 6, 8, 3, 14, 7, 0)!, - ), - ( - period { hours = 1, ... }, - new(chrono::UTC, 0, 2022, 2, 4, 4, 14, 7, 0)!, - ), - ( - period { hours = 24, ... }, - new(chrono::UTC, 0, 2022, 2, 5, 3, 14, 7, 0)!, - ), - ( - period { hours = 25, ... }, - new(chrono::UTC, 0, 2022, 2, 5, 4, 14, 7, 0)!, - ), - ( - period { hours = 123456, ... }, - new(chrono::UTC, 0, 2036, 3, 6, 3, 14, 7, 0)!, - ), - ( - period { hours = -2, ... }, - new(chrono::UTC, 0, 2022, 2, 4, 1, 14, 7, 0)!, - ), - ( - period { hours = -24, ... }, - new(chrono::UTC, 0, 2022, 2, 3, 3, 14, 7, 0)!, - ), - ( - period { hours = -123456, ... }, - new(chrono::UTC, 0, 2008, 1, 5, 3, 14, 7, 0)!, - ), - ( - period { seconds = 2, ... }, - new(chrono::UTC, 0, 2022, 2, 4, 3, 14, 9, 0)!, - ), - ( - period { seconds = 666666666, ... }, - new(chrono::UTC, 0, 2043, 3, 22, 4, 25, 13, 0)!, - ), - ( - period { seconds = -2, ... }, - new(chrono::UTC, 0, 2022, 2, 4, 3, 14, 5, 0)!, - ), - ( - period { seconds = -666666666, ... }, - new(chrono::UTC, 0, 2000, 12, 20, 2, 3, 1, 0)!, - ), - ( - period { nanoseconds = 123, ... }, - new(chrono::UTC, 0, 2022, 2, 4, 3, 14, 7, 123)!, - ), - ( - period { nanoseconds = 1361661361461, ... }, - new(chrono::UTC, 0, 2022, 2, 4, 3, 36, 48, 661361461)!, - ), - ( - period { nanoseconds = -1361661361461, ... }, - new(chrono::UTC, 0, 2022, 2, 4, 2, 51, 25, 338638539)!, - ), - ( - period { months = 1, seconds = -666666666, ... }, - new(chrono::UTC, 0, 2001, 1, 17, 2, 3, 1, 0)!, - ), - ( - period { months = 1, seconds = -666666666, ... }, - new(chrono::UTC, 0, 2001, 1, 17, 2, 3, 1, 0)!, - ), - ( - period { - years = -1, - months = -2, - weeks = -3, - days = -4, - hours = -5, - minutes = -6, - seconds = -7, - nanoseconds = -8, - ... - }, - new(chrono::UTC, 0, 2020, 11, 8, 22, 7, 59, 999999992)!, - ), - ( - period { - years = 1, - months = 2, - weeks = 3, - days = 4, - hours = 5, - minutes = 6, - seconds = 7, - nanoseconds = 8, - ... - }, - new(chrono::UTC, 0, 2023, 4, 29, 8, 20, 14, 8)!, - ), - ( - period { - years = 1, - months = -2, - weeks = 3, - days = -5, - hours = 8, - minutes = -13, - seconds = 21, - nanoseconds = -34, - ... - }, - new(chrono::UTC, 0, 2022, 12, 20, 11, 1, 27, 999999966)!, - ), - ( - period { - years = -1, - months = 12, - weeks = -52, - days = -31, - hours = 24, - minutes = -3600, - seconds = 3600, - nanoseconds = -86400000000000, - ... - }, - new(chrono::UTC, 0, 2021, 1, 2, 16, 14, 7, 0)!, - ), - ]; - for (let i = 0z; i < len(cases); i += 1) { - const p = cases[i].0; - const expected = cases[i].1; - const actual = add(d, calculus::DEFAULT, p); - assert(eq(actual, expected), "addition miscalculation"); - }; -}; - -@test fn sub() void = { - const d = new(chrono::UTC, 0, 2022, 2, 4, 3, 14, 7, 0)!; - const cases = [ - ( - period { years = 1, ... }, - new(chrono::UTC, 0, 2021, 2, 4, 3, 14, 7, 0)!, - ), - ( - period { months = 2, ... }, - new(chrono::UTC, 0, 2021, 12, 4, 3, 14, 7, 0)!, - ), - ( - period { months = 14, ... }, - new(chrono::UTC, 0, 2020, 12, 4, 3, 14, 7, 0)!, - ), - ]; - for (let i = 0z; i < len(cases); i += 1) { - const p = cases[i].0; - const expected = cases[i].1; - const actual = sub(d, calculus::DEFAULT, p); - assert(eq(actual, expected), "subtraction miscalculation"); - }; + assert(chrono::eq( + &truncate(dt, unit::ERA), + &new(chrono::UTC, 0, 1, 1, 1, 0, 0, 0, 0)!)!, + "invalid truncate() result 01"); + + assert(chrono::eq( + &truncate(dt, unit::YEAR), + &new(chrono::UTC, 0, 1994, 1, 1, 0, 0, 0, 0)!)!, + "invalid truncate() result 02"); + + assert(chrono::eq( + &truncate(dt, unit::MONTH), + &new(chrono::UTC, 0, 1994, 8, 1, 0, 0, 0, 0)!)!, + "invalid truncate() result 03"); + + assert(chrono::eq( + &truncate(dt, unit::WEEK), + &new(chrono::UTC, 0, 1994, 8, 22, 0, 0, 0, 0)!)!, + "invalid truncate() result 04"); + + assert(chrono::eq( + &truncate(dt, unit::DAY), + &new(chrono::UTC, 0, 1994, 8, 27, 0, 0, 0, 0)!)!, + "invalid truncate() result 05"); + + assert(chrono::eq( + &truncate(dt, unit::HOUR), + &new(chrono::UTC, 0, 1994, 8, 27, 11, 0, 0, 0)!)!, + "invalid truncate() result 06"); + + assert(chrono::eq( + &truncate(dt, unit::MINUTE), + &new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 0, 0)!)!, + "invalid truncate() result 07"); + + assert(chrono::eq( + &truncate(dt, unit::SECOND), + &new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!)!, + "invalid truncate() result 08"); + + assert(chrono::eq( + &truncate(dt, unit::NANOSECOND), + &dt)!, + "invalid truncate() result 09"); }; diff --git a/datetime/chronology.ha b/datetime/chronology.ha @@ -7,15 +7,6 @@ use time::chrono; // These functions are renamed to avoid namespace conflicts, like in the // parameters of the [[new]] function. -// TODO: For [[epochal]]: Use Hare epoch or Gregorian epoch? Make two function? -// TODO: Create an exported [[zeroweekday]] field function. - -// Returns a [[datetime]]'s number of days since the calendar epoch 0000-01-01. -export fn epochal(dt: *datetime) chrono::date = _epochal(dt); - -// Returns a [[datetime]]'s number of seconds since the Unix epoch 1970-01-01. -export fn epochunix(dt: *datetime) int = _epochunix(dt); - // Returns a [[datetime]]'s era. export fn era(dt: *datetime) int = _era(dt); @@ -28,7 +19,7 @@ export fn month(dt: *datetime) int = _month(dt); // Returns a [[datetime]]'s day of the month. export fn day(dt: *datetime) int = _day(dt); -// Returns a [[datetime]]'s day of the week. +// Returns a [[datetime]]'s day of the week; Monday=0 to Sunday=6. export fn weekday(dt: *datetime) int = _weekday(dt); // Returns a [[datetime]]'s ordinal day of the year. @@ -58,14 +49,6 @@ export fn second(dt: *datetime) int = _second(dt); // Returns a [[datetime]]'s nanosecond of the second. export fn nanosecond(dt: *datetime) int = _nanosecond(dt); -fn _epochal(dt: *datetime) chrono::date = { - return chrono::getdate(dt) - EPOCHAL_GREGORIAN; -}; - -fn _epochunix(dt: *datetime) int = { - return time::unix(*(dt: *time::instant)): int; -}; - fn _era(dt: *datetime) int = { match (dt.era) { case void => @@ -82,7 +65,7 @@ fn _era(dt: *datetime) int = { fn _year(dt: *datetime) int = { match (dt.year) { case void => - const ymd = calc_ymd(chrono::getdate(dt)); + const ymd = calc_ymd(chrono::date(dt)); dt.year = ymd.0; dt.month = ymd.1; dt.day = ymd.2; @@ -95,7 +78,7 @@ fn _year(dt: *datetime) int = { fn _month(dt: *datetime) int = { match (dt.month) { case void => - const ymd = calc_ymd(chrono::getdate(dt)); + const ymd = calc_ymd(chrono::date(dt)); dt.year = ymd.0; dt.month = ymd.1; dt.day = ymd.2; @@ -108,7 +91,7 @@ fn _month(dt: *datetime) int = { fn _day(dt: *datetime) int = { match (dt.day) { case void => - const ymd = calc_ymd(chrono::getdate(dt)); + const ymd = calc_ymd(chrono::date(dt)); dt.year = ymd.0; dt.month = ymd.1; dt.day = ymd.2; @@ -121,7 +104,7 @@ fn _day(dt: *datetime) int = { fn _weekday(dt: *datetime) int = { match (dt.weekday) { case void => - dt.weekday = calc_weekday(chrono::getdate(dt)); + dt.weekday = calc_weekday(chrono::date(dt)); return dt.weekday: int; case let y: int => return y; @@ -238,7 +221,7 @@ fn _isoweek(dt: *datetime) int = { fn _hour(dt: *datetime) int = { match (dt.hour) { case void => - const hmsn = calc_hmsn(chrono::gettime(dt)); + const hmsn = calc_hmsn(chrono::time(dt)); dt.hour = hmsn.0; dt.minute = hmsn.1; dt.second = hmsn.2; @@ -252,7 +235,7 @@ fn _hour(dt: *datetime) int = { fn _minute(dt: *datetime) int = { match (dt.minute) { case void => - const hmsn = calc_hmsn(chrono::gettime(dt)); + const hmsn = calc_hmsn(chrono::time(dt)); dt.hour = hmsn.0; dt.minute = hmsn.1; dt.second = hmsn.2; @@ -266,7 +249,7 @@ fn _minute(dt: *datetime) int = { fn _second(dt: *datetime) int = { match (dt.second) { case void => - const hmsn = calc_hmsn(chrono::gettime(dt)); + const hmsn = calc_hmsn(chrono::time(dt)); dt.hour = hmsn.0; dt.minute = hmsn.1; dt.second = hmsn.2; @@ -280,7 +263,7 @@ fn _second(dt: *datetime) int = { fn _nanosecond(dt: *datetime) int = { match (dt.nanosecond) { case void => - const hmsn = calc_hmsn(chrono::gettime(dt)); + const hmsn = calc_hmsn(chrono::time(dt)); dt.hour = hmsn.0; dt.minute = hmsn.1; dt.second = hmsn.2; diff --git a/datetime/date.ha b/datetime/date.ha @@ -15,7 +15,7 @@ export def EPOCHAL_JULIAN: i64 = -2440588; export def EPOCHAL_GREGORIAN: i64 = -719164; // Calculates whether a year is a leap year. -export fn is_leap_year(y: int) bool = { +export fn isleapyear(y: int) bool = { return if (y % 4 != 0) false else if (y % 100 != 0) true else if (y % 400 != 0) false @@ -25,49 +25,41 @@ export fn is_leap_year(y: int) bool = { // 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_n_days_in_month(y, m); + d <= calc_month_daycnt(y, m); }; // 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_n_days_in_year(y); + return yd >= 1 && yd <= calc_year_daycnt(y); }; // Calculates the number of days in the given month of the given year. -fn calc_n_days_in_month(y: int, m: int) int = { +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 ]; if (m == 2) { - if (is_leap_year(y)) { - return 29; - } else { - return 28; - }; + 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_n_days_in_year(y: int) int = { - if (is_leap_year(y)) { - return 366; - } else { - return 365; - }; +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 Gegorian cycle) + 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 + 1; + return wd; }; // Calculates the era, given a year. @@ -80,7 +72,7 @@ fn calc_era(y: int) int = { }; // Calculates the year, month, and day-of-month, given an epochal day. -fn calc_ymd(e: chrono::date) (int, int, int) = { +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 // @@ -122,7 +114,7 @@ fn calc_yearday(y: int, m: int, d: int) int = { 273, 304, 334, ]; - if (m >= 3 && is_leap_year(y)) { + if (m >= 3 && isleapyear(y)) { return months_firsts[m - 1] + d + 1; } else { return months_firsts[m - 1] + d; @@ -133,22 +125,22 @@ fn calc_yearday(y: int, m: int, d: int) int = { // given a year, month, day-of-month, and day-of-week. fn calc_isoweekyear(y: int, m: int, d: int, wd: int) int = { if ( - // if the date is within a week whose Thurday - // belongs to the previous gregorian year + // if the date is within a week whose Thursday + // belongs to the previous Gregorian year m == 1 && ( - (d == 1 && (wd == 5 || wd == 6 || wd == 7)) - || (d == 2 && (wd == 6 || wd == 7)) - || (d == 3 && wd == 7) + (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 Thurday - // belongs to the next gregorian year + // if the date is within a week whose Thursday + // belongs to the next Gregorian year m == 12 && ( - (d == 29 && wd == 1) - || (d == 30 && (wd == 1 || wd == 2)) - || (d == 31 && (wd == 1 || wd == 2 || wd == 3)) + (d == 29 && wd == 0) + || (d == 30 && (wd == 0 || wd == 1)) + || (d == 31 && (wd == 0 || wd == 1 || wd == 2)) ) ) { return y + 1; @@ -160,64 +152,48 @@ fn calc_isoweekyear(y: int, m: int, d: int, wd: int) int = { // Calculates the ISO week, // given a year, week, day-of-week, and day-of-year. fn calc_isoweek(y: int, w: int) int = { - const jan1wd = calc_janfirstweekday(y); - const iw = if (jan1wd == 1) { - yield w; - } else if (jan1wd == 2 || jan1wd == 3 || jan1wd == 4) { - yield w + 1; - } else { - yield if (w == 0) { - yield if (jan1wd == 5) { - yield 53; - } else if (jan1wd == 6) { - yield if (is_leap_year(y - 1)) { - yield 53; - } else { - yield 52; - }; - } else if (jan1wd == 7) { - yield 52; - } else { - // all jan1wd values exhausted - abort("Unreachable"); - }; - } else { - yield w; + 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"); }; - return iw; }; // 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 + 7 - wd) / 7; + 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 % 7)) / 7; + return (yd + 6 - ((wd + 1) % 7)) / 7; }; // Calculates the day-of-week, given a epochal day, -// from Monday=1 to Sunday=7. -fn calc_weekday(e: chrono::date) int = { - const wd = ((e + 3) % 7 + 1): int; - return if (wd > 0) wd else wd + 7; -}; - -// Calculates the zero-indexed day-of-week, given a day-of-week, // from Monday=0 to Sunday=6. -fn calc_zeroweekday(wd: int) int = { - return wd - 1; +fn calc_weekday(e: i64) int = { + const wd = ((e + 3) % 7): int; + return (wd + 7) % 7; }; -// Calculates the [[chrono::date]], +// Calculates the date, // given a year, month, and day-of-month. -fn calc_date_from_ymd(y: int, m: int, d: int) (chrono::date | invalid) = { +fn calc_date__ymd(y: int, m: int, d: int) (i64 | invalid) = { if (!is_valid_ymd(y, m, d)) { return invalid; }; @@ -236,24 +212,24 @@ fn calc_date_from_ymd(y: int, m: int, d: int) (chrono::date | invalid) = { return e; }; -// Calculates the [[chrono::date]], +// Calculates the date, // given a year, week, and day-of-week. -fn calc_date_from_ywd(y: int, w: int, wd: int) (chrono::date | invalid) = { +fn calc_date__ywd(y: int, w: int, wd: int) (i64 | invalid) = { const jan1wd = calc_janfirstweekday(y); - const yd = wd - jan1wd + 1 + 7 * w; - return calc_date_from_yd(y, yd)?; + const yd = wd - jan1wd + 7 * w; + return calc_date__yd(y, yd)?; }; -// Calculates the [[chrono::date]], +// Calculates the date, // given a year and day-of-year. -fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { - if (yd < 1 || yd > calc_n_days_in_year(y)) { +fn calc_date__yd(y: int, yd: int) (i64 | invalid) = { + if (yd < 1 || yd > calc_year_daycnt(y)) { return invalid; }; - return calc_date_from_ymd(y, 1, 1)? + yd - 1; + return calc_date__ymd(y, 1, 1)? + yd - 1; }; -@test fn calc_date_from_ymd() void = { +@test fn calc_date__ymd() void = { const cases = [ (( -768, 2, 5), -999999, false), (( -1, 12, 31), -719529, false), @@ -289,20 +265,20 @@ fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { const params = cases[i].0; const expect = cases[i].1; const should_error = cases[i].2; - const actual = calc_date_from_ymd( + const actual = calc_date__ymd( params.0, params.1, params.2, ); if (should_error) { assert(actual is invalid, "invalid date accepted"); } else { - assert(actual is chrono::date, "valid date not accepted"); - assert(actual as chrono::date == expect, "date miscalculation"); + assert(actual is i64, "valid date not accepted"); + assert(actual as i64 == expect, "date miscalculation"); }; }; }; -@test fn calc_date_from_ywd() void = { +@test fn calc_date__ywd() void = { const cases = [ (( -768, 0, 4), -1000034), (( -768, 5, 4), -999999), @@ -340,13 +316,13 @@ fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { for (let i = 0z; i < len(cases); i += 1) { const ywd = cases[i].0; const expected = cases[i].1; - const actual = calc_date_from_ywd(ywd.0, ywd.1, ywd.2)!; + const actual = calc_date__ywd(ywd.0, ywd.1, ywd.2)!; assert(actual == expected, - "incorrect calc_date_from_ywd() result"); + "incorrect calc_date__ywd() result"); }; }; -@test fn calc_date_from_yd() void = { +@test fn calc_date__yd() void = { const cases = [ ( -768, 36, -999999), ( -1, 365, -719529), @@ -375,14 +351,14 @@ fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { const y = cases[i].0; const yd = cases[i].1; const expected = cases[i].2; - const actual = calc_date_from_yd(y, yd)!; + const actual = calc_date__yd(y, yd)!; assert(expected == actual, "error in date calculation from yd"); }; - assert(calc_date_from_yd(2020, 0) is invalid, - "calc_date_from_yd() did not reject invalid yearday"); - assert(calc_date_from_yd(2020, 400) is invalid, - "calc_date_from_yd() did not reject invalid yearday"); + assert(calc_date__yd(2020, 0) is invalid, + "calc_date__yd() did not reject invalid yearday"); + assert(calc_date__yd(2020, 400) is invalid, + "calc_date__yd() did not reject invalid yearday"); }; @test fn calc_ymd() void = { @@ -454,21 +430,21 @@ fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { @test fn calc_week() void = { const cases = [ - ((1, 1), 1), - ((1, 2), 0), - ((1, 3), 0), - ((1, 4), 0), - ((1, 5), 0), - ((1, 6), 0), - ((1, 7), 0), - ((21, 2), 3), - ((61, 3), 9), - ((193, 5), 27), - ((229, 1), 33), - ((286, 4), 41), - ((341, 7), 48), - ((365, 6), 52), - ((366, 1), 53), + (( 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) { @@ -481,21 +457,21 @@ fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { @test fn calc_sundayweek() void = { const cases = [ - ((1, 1), 0), - ((1, 2), 0), - ((1, 3), 0), - ((1, 4), 0), - ((1, 5), 0), - ((1, 6), 0), - ((1, 7), 1), - ((21, 2), 3), - ((61, 3), 9), - ((193, 5), 27), - ((229, 1), 33), - ((286, 4), 41), - ((341, 7), 49), - ((365, 6), 52), - ((366, 1), 53), + (( 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) { @@ -508,27 +484,27 @@ fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { @test fn calc_weekday() void = { const cases = [ - (-999999, 4), // -0768-02-05 - (-719529, 5), // -0001-12-31 - (-719528, 6), // 0000-01-01 - (-719527, 7), // 0000-01-02 - (-719163, 7), // 0000-12-31 - (-719162, 1), // 0001-01-01 - (-719161, 2), // 0001-01-02 - ( -1745, 2), // 1965-03-23 - ( -1, 3), // 1969-12-31 - ( 0, 4), // 1970-01-01 - ( 1, 5), // 1970-01-02 - ( 10956, 5), // 1999-12-31 - ( 10957, 6), // 2000-01-01 - ( 10958, 7), // 2000-01-02 - ( 24854, 1), // 2038-01-18 - ( 24855, 2), // 2038-01-19 - ( 24856, 3), // 2038-01-20 - ( 100000, 2), // 2243-10-17 - ( 999999, 4), // 4707-11-28 - (1000000, 5), // 4707-11-29 - (9999999, 6), // 29349-01-25 + (-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 ]; for (let i = 0z; i < len(cases); i += 1) { const paramt = cases[i].0; @@ -541,77 +517,77 @@ fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = { @test fn calc_janfirstweekday() void = { const cases = [ // year weekday - (1969, 3), - (1970, 4), - (1971, 5), - (1972, 6), - (1973, 1), - (1974, 2), - (1975, 3), - (1976, 4), - (1977, 6), - (1978, 7), - (1979, 1), - (1980, 2), - (1981, 4), - (1982, 5), - (1983, 6), - (1984, 7), - (1985, 2), - (1986, 3), - (1987, 4), - (1988, 5), - (1989, 7), - (1990, 1), - (1991, 2), - (1992, 3), - (1993, 5), - (1994, 6), - (1995, 7), - (1996, 1), - (1997, 3), - (1998, 4), - (1999, 5), - (2000, 6), - (2001, 1), - (2002, 2), - (2003, 3), - (2004, 4), - (2005, 6), - (2006, 7), - (2007, 1), - (2008, 2), - (2009, 4), - (2010, 5), - (2011, 6), - (2012, 7), - (2013, 2), - (2014, 3), - (2015, 4), - (2016, 5), - (2017, 7), - (2018, 1), - (2019, 2), - (2020, 3), - (2021, 5), - (2022, 6), - (2023, 7), - (2024, 1), - (2025, 3), - (2026, 4), - (2027, 5), - (2028, 6), - (2029, 1), - (2030, 2), - (2031, 3), - (2032, 4), - (2033, 6), - (2034, 7), - (2035, 1), - (2036, 2), - (2037, 4), - (2038, 5), - (2039, 6), + (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; diff --git a/datetime/datetime.ha b/datetime/datetime.ha @@ -8,6 +8,24 @@ use time::chrono; // Invalid [[datetime]]. export type invalid = !chrono::invalid; +// A date/time object; a [[time::chrono::moment]] wrapper optimized for the +// Gregorian chronology +// +// It is by extension a [[time::instant]] wrapper, and carries information about +// its [[time::chrono::timescale]] and [[time::chrono::locality]]. +// +// 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 datetime observes various chronological values, cached in its fields. To +// evaluate and obtain these values, use the various "observe" functions +// ([[year]], [[day]], 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 +// datetime, which waives guarantees of validity. export type datetime = struct { chrono::moment, @@ -29,12 +47,12 @@ export type datetime = struct { }; fn init() datetime = datetime { - loc = chrono::LOCAL, sec = 0, nsec = 0, + loc = chrono::UTC, + zone = null, date = void, time = void, - zone = void, era = void, year = void, @@ -53,28 +71,51 @@ fn init() datetime = datetime { nanosecond = void, }; -// Creates a new datetime. When loc=void, defaults to chrono::local. +// Evaluates and populates all of a [[datetime]]'s fields. +fn all(dt: *datetime) *datetime = { + _era(dt); + _year(dt); + _month(dt); + _day(dt); + _yearday(dt); + _isoweekyear(dt); + _isoweek(dt); + _week(dt); + _sundayweek(dt); + _weekday(dt); + + _hour(dt); + _minute(dt); + _second(dt); + _nanosecond(dt); + + return dt; +}; + +// Creates a new datetime. 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 Jan 1st 00:00:00.000000000 +0000 UTC +// // 0000-01-01 00:00:00.000000000 +0000 UTC UTC // datetime::new(time::chrono::UTC, 0); // -// // 2038 Jan 19th 03:14:07.000000618 +0000 UTC -// datetime::new(time::chrono::UTC, 0, 2038, 1, 19, 3, 14, 7, 618); +// // 2019-12-27 20:07:08.000031415 +0000 UTC UTC +// datetime::new(time::chrono::UTC, 0, 2019, 12, 27, 20, 07, 08, 31415); // -// // 2038 Jan 19th 02:00:00.000000000 +0100 Europe/Amsterdam -// datetime::new(&time::chrono::tz("Europe/Amsterdam"), 1 * time::HOUR, -// 2038, 1, 19, 2); +// // 2019-12-27 21:00:00.000000000 +0100 CET Europe/Amsterdam +// datetime::new(time::chrono::tz("Europe/Amsterdam")!, 1 * time::HOUR, +// 2019, 12, 27, 21); // -// 'offs' is the zone offset from the normal timezone (in most cases, UTC). For -// example, the "Asia/Tokyo" timezone has a single zoffset of +9 hours, but the -// "Australia/Sydney" timezone has zoffsets +10 hours and +11 hours, as they +// '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), 'offs' must match one of the timezone's observed -// zoffsets, or will fail. See [[time::chrono::fixedzone]] for custom timezones. +// 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 zoffset. If the givem timezone has a single zone, [[new]] -// will use that zone's zoffset. Otherwise [[new]] will try to infer the zoffset +// 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 datetimes are ambiguous or nonexistent. For example: // @@ -95,30 +136,29 @@ export fn new( // - Implement as described. // - fix calls with `years <= -4715`. // https://todo.sr.ht/~sircmpwn/hare/565 - let defaults: [_]int = [ + let _fields: [_]int = [ 0, 1, 1, // year month day 0, 0, 0, 0, // hour min sec nsec ]; - if (len(fields) > len(defaults)) { - // cannot specify more than 7 fields - return invalid; + if (len(fields) > len(_fields)) { + abort("datetime::new(): Too many field arguments"); }; for (let i = 0z; i < len(fields); i += 1) { - defaults[i] = fields[i]; + _fields[i] = fields[i]; }; - const year = defaults[0]; - const month = defaults[1]; - const day = defaults[2]; - const hour = defaults[3]; - const min = defaults[4]; - const sec = defaults[5]; - const nsec = defaults[6]; + 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_date_from_ymd(year, month, day)?; - const mtime = calc_time_from_hmsn(hour, min, sec, nsec)?; + const mdate = calc_date__ymd(year, month, day)?; + const mtime = calc_time__hmsn(hour, min, sec, nsec)?; // create the moment const m = match (zo) { @@ -134,33 +174,41 @@ export fn new( }; const dt = from_moment(m); + + const zo = match (zo) { + case void => + yield chrono::mzone(&m).zoff; + case let d: time::duration => + yield d; + }; + + // check if input values are actually observed if ( - year == _year(&dt) - && month == _month(&dt) - && day == _day(&dt) - && hour == _hour(&dt) - && min == _minute(&dt) - && sec == _second(&dt) - && nsec == _nanosecond(&dt) + zo != chrono::mzone(&dt).zoff + || year != _year(&dt) + || month != _month(&dt) + || day != _day(&dt) + || hour != _hour(&dt) + || min != _minute(&dt) + || sec != _second(&dt) + || nsec != _nanosecond(&dt) ) { - void; - } else { return invalid; }; + return dt; }; -// Returns a [[datetime]] of the current system time, -// using [[time::clock::REALTIME]] and [[time::chrono::LOCAL]]. +// Returns a [[datetime]] of the current system time using +// [[time::clock::REALTIME]], in the [[time::chrono::LOCAL]] locality. export fn now() datetime = { - // TODO: Consider adding function parameters. - // Should [[now]] specify appropriate params like a time::clock and - // chrono::timezone? Perhaps a separate function, [[from_clock]]. - // - // https://todo.sr.ht/~sircmpwn/hare/645 - const i = time::now(time::clock::REALTIME); - const m = chrono::new(chrono::LOCAL, i); - return from_moment(m); + return from_instant(chrono::LOCAL, time::now(time::clock::REALTIME)); +}; + +// Returns a [[datetime]] of the current system time using +// [[time::clock::REALTIME]], in the [[time::chrono::UTC]] locality. +export fn nowutc() datetime = { + return from_instant(chrono::UTC, time::now(time::clock::REALTIME)); }; // Creates a [[datetime]] from a [[time::chrono::moment]]. @@ -181,104 +229,25 @@ export fn from_instant(loc: chrono::locality, i: time::instant) datetime = { return from_moment(chrono::new(loc, i)); }; -// Creates a [[datetime]] from a string, parsed according to a layout, -// using [[strategy::ALL]], or otherwise fails. -export fn from_str(layout: str, s: str) (datetime | insufficient | invalid) = { - // XXX: Should we allow the user to specify [[strategy]] for security? - const b = newbuilder(); - parse(&b, layout, s)?; - return finish(&b)?; -}; - -// A [[builder]] has insufficient information and cannot create a valid datetime. -export type insufficient = !void; - -// A pseudo-datetime; a [[datetime]] which may hold invalid values, and does not -// guarantee internal validity or consistency. -// -// This can be used to construct new [[datetime]]s. Start with [[newbuilder]], -// then collect enough datetime information incrementally by direct field -// assignments and/or one or more calls to [[parse]]. Finish with [[finish]]. +// Creates a [[datetime]] from a string, parsed according to a layout format. +// See [[parse]] and [[format]]. // -// let builder = datetime::newbuilder(); -// datetime::parse(&builder, "Year: %Y", "Year: 2038"); -// datetime::parse(&builder, "Month: %m", "Month: 01"); -// builder.day = 19; -// let dt = datetime::finish(&builder, datetime::strategy::YMD); +// let new = datetime::from_str( +// datetime::STAMP_NOZL, +// "2019-12-27 22:07:08.000000000 +0100 CET Europe/Amsterdam", +// locs... +// )!; // -export type builder = datetime; - -// Creates a new [[builder]]. -export fn newbuilder() builder = init(): builder; - -// Returns a [[datetime]] from a [[builder]]. The provided [[strategy]]s will be -// tried in order until a valid datetime is produced, or otherwise fail. The -// default strategy is [[strategy::ALL]]. -export fn finish(f: *builder, m: strategy...) (datetime | insufficient | invalid) = { - if (len(m) == 0) { - m = [strategy::ALL]; - }; - - for (let i = 0z; i < len(m); i += 1) { - const M = m[i]; - if ( - M & strategy::YMD != 0 && - f.year is int && - f.month is int && - f.day is int - ) { - f.date = calc_date_from_ymd( - f.year as int, - f.month as int, - f.day as int, - )?; - return *f: datetime; - }; - - if ( - M & strategy::YD != 0 && - f.year is int && - f.yearday is int - ) { - f.date = calc_date_from_yd( - f.year as int, - f.yearday as int, - )?; - return *f: datetime; - }; - - if ( - M & strategy::YWD != 0 && - f.year is int && - f.week is int && - f.weekday is int - ) { - f.date = calc_date_from_ywd( - f.year as int, - f.week as int, - f.weekday as int, - )?; - return *f: datetime; - }; - - // TODO: calendar.ha: calc_date_from_isoywd() - }; - - return insufficient; -}; - -// Specifies which [[builder]] fields and what strategy to use to calculate the -// date, and thus a valid [[datetime]]. -export type strategy = enum uint { - // year, month, day - YMD = 1 << 0, - // year, yearday - YD = 1 << 1, - // year, week, weekday - YWD = 1 << 2, - // isoyear, isoweek, weekday - ISOYWD = 1 << 4, - - // all strategies, in order as presented here - ALL = YMD | YD | YWD | ISOYWD, +// The datetime'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... +) (datetime | parsefail | insufficient | invalid) = { + const v = newvirtual(); + parse(&v, layout, s)?; + return realize(v, locs...)?; }; diff --git a/datetime/duration.ha b/datetime/duration.ha @@ -0,0 +1,11 @@ +// License: MPL-2.0 +// (c) 2023 Byron Torres <b@torresjrjr.com> +use time; + +// Adds a [[time::duration]] to a [[datetime]] with [[time::add]]. +// +// See [[reckon]] for a chronology-wise arithmetic operation which uses +// [[period]]. +export fn add(a: datetime, d: time::duration) datetime = { + return from_instant(a.loc, time::add(*(&a: *time::instant), d)); +}; diff --git a/datetime/errors.ha b/datetime/errors.ha @@ -0,0 +1,18 @@ +// License: MPL-2.0 +// (c) 2023 Byron Torres <b@torresjrjr.com> + +// All possible errors returned from [[datetime]]. +export type error = !(insufficient | invalid | parsefail); + +// Converts an [[error]] into a human-friendly string. +export fn strerror(err: error) const str = { + match (err) { + case insufficient => + return "Insufficient datetime information"; + case invalid => + return "Invalid datetime information"; + case let rn: parsefail => + // TODO: use rune 'rn' here + return "Datetime parsing error"; + }; +}; diff --git a/datetime/format.ha b/datetime/format.ha @@ -6,9 +6,9 @@ use ascii; use errors; use fmt; use io; -use strconv; use strings; use strio; +use time; use time::chrono; // [[datetime::format]] layout for the email date format. @@ -32,6 +32,10 @@ export def STAMP: str = "%Y-%m-%d %H:%M:%S"; // [[datetime::format]] layout for a simple timestamp with nanoseconds. export def STAMP_NANO: str = "%Y-%m-%d %H:%M:%S.%N"; +// [[datetime::format]] layout for a simple timestamp with nanoseconds and zone +// offset. +export def STAMP_ZOFF: str = "%Y-%m-%d %H:%M:%S.%N %z"; + // [[datetime::format]] layout for a simple timestamp with nanoseconds, // zone offset, zone abbreviation, and locality. export def STAMP_NOZL: str = "%Y-%m-%d %H:%M:%S.%N %z %Z %L"; @@ -83,7 +87,7 @@ export fn bsformat( buf: []u8, layout: str, dt: *datetime, -) (str | invalid | io::error) = { +) (str | io::error) = { let sink = strio::fixed(buf); format(&sink, layout, dt)?; return strio::string(&sink); @@ -91,7 +95,7 @@ export fn bsformat( // Formats a [[datetime]] and writes it into a heap-allocated string. // The caller must free the return value. -export fn asformat(layout: str, dt: *datetime) (str | invalid | io::error) = { +export fn asformat(layout: str, dt: *datetime) (str | io::error) = { let sink = strio::dynamic(); format(&sink, layout, dt)?; return strio::string(&sink); @@ -100,70 +104,61 @@ export fn asformat(layout: str, dt: *datetime) (str | invalid | io::error) = { fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = { switch (r) { case 'a' => - return fmt::fprint(out, WEEKDAYS_SHORT[weekday(dt) - 1]); + return fmt::fprint(out, WEEKDAYS_SHORT[_weekday(dt)]); case 'A' => - return fmt::fprint(out, WEEKDAYS[weekday(dt) - 1]); + return fmt::fprint(out, WEEKDAYS[_weekday(dt)]); case 'b' => - return fmt::fprint(out, MONTHS_SHORT[month(dt) - 1]); + return fmt::fprint(out, MONTHS_SHORT[_month(dt) - 1]); case 'B' => - return fmt::fprint(out, MONTHS[month(dt) - 1]); + return fmt::fprint(out, MONTHS[_month(dt) - 1]); case 'd' => - return fmt::fprintf(out, "{:02}", day(dt)); + return fmt::fprintf(out, "{:02}", _day(dt)); case 'F' => - return fmt::fprintf(out, "{:04}-{:02}-{:02}", year(dt), month(dt), day(dt)); + return fmt::fprintf(out, "{:04}-{:02}-{:02}", _year(dt), _month(dt), _day(dt)); case 'H' => - return fmt::fprintf(out, "{:02}", hour(dt)); + return fmt::fprintf(out, "{:02}", _hour(dt)); case 'I' => - return fmt::fprintf(out, "{:02}", hour12(dt)); + return fmt::fprintf(out, "{:02}", (_hour(dt) + 11) % 12 + 1); case 'j' => - return fmt::fprint(out, strconv::itos(yearday(dt))); + return fmt::fprintf(out, "{:03}", _yearday(dt)); case 'L' => return fmt::fprint(out, dt.loc.name); case 'm' => - return fmt::fprintf(out, "{:02}", month(dt)); + return fmt::fprintf(out, "{:02}", _month(dt)); case 'M' => - return fmt::fprintf(out, "{:02}", minute(dt)); + return fmt::fprintf(out, "{:02}", _minute(dt)); case 'N' => - return fmt::fprintf(out, "{:09}", strconv::itos(nanosecond(dt))); + return fmt::fprintf(out, "{:09}", _nanosecond(dt)); case 'p' => - const s = if (hour(dt) < 12) { - yield "AM"; - } else { - yield "PM"; - }; - return fmt::fprint(out, s); + return fmt::fprint(out, if (_hour(dt) < 12) "AM" else "PM"); case 's' => - return fmt::fprintf(out, "{:02}", epochunix(dt)); + return fmt::fprintf(out, "{:02}", time::unix(*(dt: *time::instant))); case 'S' => - return fmt::fprintf(out, "{:02}", second(dt)); + return fmt::fprintf(out, "{:02}", _second(dt)); case 'T' => - return fmt::fprintf(out, "{:02}:{:02}:{:02}", hour(dt), minute(dt), second(dt)); + return fmt::fprintf(out, "{:02}:{:02}:{:02}", _hour(dt), _minute(dt), _second(dt)); case 'u' => - return fmt::fprint(out, strconv::itos(weekday(dt))); + return fmt::fprintf(out, "{}", _weekday(dt) + 1); case 'U' => return fmt::fprintf(out, "{:02}", _sundayweek(dt)); case 'w' => - return fmt::fprint(out, strconv::itos(weekday(dt) % 7)); + return fmt::fprintf(out, "{}", (_weekday(dt) + 1) % 7); case 'W' => - return fmt::fprintf(out, "{:02}", week(dt)); + return fmt::fprintf(out, "{:02}", _week(dt)); case 'y' => - let year_str = strconv::itos(year(dt)); - year_str = strings::sub(year_str, len(year_str) - 2, strings::end); - return fmt::fprint(out, year_str); + return fmt::fprintf(out, "{:02}", _year(dt) % 100); case 'Y' => - return fmt::fprint(out, strconv::itos(year(dt))); + return fmt::fprintf(out, "{:04}", _year(dt)); case 'z' => - // TODO: test me - let pm = '+'; - const z = if (chrono::getzone(dt).zoffset >= 0) { - yield calc_hmsn(chrono::getzone(dt).zoffset); + const (sign, zo) = if (chrono::mzone(dt).zoff >= 0) { + yield ('+', calc_hmsn(chrono::mzone(dt).zoff)); } else { - pm = '-'; - yield calc_hmsn(-chrono::getzone(dt).zoffset); + yield ('-', calc_hmsn(-chrono::mzone(dt).zoff)); }; - return fmt::fprintf(out, "{}{:02}{:02}", pm, z.0, z.1); + const (hr, mi) = (zo.0, zo.1); + return fmt::fprintf(out, "{}{:02}{:02}", sign, hr, mi); case 'Z' => - return fmt::fprint(out, chrono::getzone(dt).abbr); + return fmt::fprint(out, chrono::mzone(dt).abbr); case '%' => return fmt::fprint(out, "%"); case => @@ -212,7 +207,7 @@ export fn format( h: io::handle, layout: str, dt: *datetime -) (size | invalid | io::error) = { +) (size | io::error) = { const iter = strings::iter(layout); let escaped = false; let n = 0z; @@ -238,85 +233,6 @@ export fn format( return n; }; -fn get_default_locale_string_index(iter: *strings::iterator, list: []str) (int | invalid) = { - const name = strings::iterstr(iter); - if (len(name) == 0) { - return invalid; - }; - for(let i = 0z; i < len(list); i += 1) { - if (strings::hasprefix(name, list[i])) { - // Consume name - for (let j = 0z; j < len(list[i]); j += 1) { - strings::next(iter); - }; - return (i: int) + 1; - }; - }; - return invalid; -}; - -fn get_max_n_digits(iter: *strings::iterator, n: uint) (int | invalid) = { - let buf: [64]u8 = [0...]; - let bufstr = strio::fixed(buf); - for (let i = 0z; i < n; i += 1) { - let r: rune = match (strings::next(iter)) { - case void => - break; - case let r: rune => - yield r; - }; - if (!ascii::isdigit(r)) { - strings::prev(iter); - break; - }; - match (strio::appendrune(&bufstr, r)) { - case io::error => - return invalid; - case => - void; - }; - }; - return match (strconv::stoi(strio::string(&bufstr))) { - case let res: int => - yield res; - case => - yield invalid; - }; -}; - -fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | invalid) = { - let s_r = match (strings::next(iter)) { - case void => - return invalid; - case let r: rune => - yield r; - }; - if (s_r == needle) { - return 1; - } else { - strings::prev(iter); - return 0; - }; -}; - -fn clamp_int(i: int, min: int, max: int) int = { - return if (i < min) { - yield min; - } else if (i > max) { - yield max; - } else { - yield i; - }; -}; - -fn hour12(dt: *datetime) int = { - let mod_hour = hour(dt) % 12; - if (mod_hour == 0) { - mod_hour = 12; - }; - return mod_hour; -}; - @test fn format() void = { const dt = new(chrono::UTC, 0, 1994, 1, 1, 2, 17, 5, 24)!; @@ -350,7 +266,7 @@ fn hour12(dt: *datetime) int = { ("%a", "Sat"), ("%A", "Saturday"), // yearday - ("%j", "1"), + ("%j", "001"), // week ("%W", "00"), // full date @@ -375,80 +291,3 @@ fn hour12(dt: *datetime) int = { }; }; }; - -// TODO: Refactor this once the rest of the parse() refactoring is done -// Ticket: https://todo.sr.ht/~sircmpwn/hare/648 - -// @test fn parse() void = { -// let dt = datetime {...}; - -// // General tests -// parse("%Y-%m-%d %H:%M:%S.%N", "1994-08-27 11:01:02.123", &dt)!; -// assert(dt.year as int == 1994 && -// dt.month as int == 8 && -// dt.day as int == 27 && -// dt.hour as int == 11 && -// dt.min as int == 1 && -// dt.sec as int == 2 && -// dt.nsec as int == 123, "invalid parsing results"); - -// // General errors -// assert(parse("%Y-%m-%d", "1a94-08-27", &dt) is invalid, -// "invalid datetime string did not throw error"); - -// assert(parse("%Y-%m-%d", "1994-123-27", &dt) is invalid, -// "invalid datetime string did not throw error"); - -// assert(parse("%Y-%m-%d", "a994-08-27", &dt) is invalid, -// "invalid datetime string did not throw error"); - -// // Basic specifiers -// parse("%a", "Tue", &dt)!; -// assert(dt.weekday as int == 2, "invalid parsing results"); - -// parse("%a %d", "Tue 27", &dt)!; -// assert(dt.weekday as int == 2 && -// dt.day as int == 27, "invalid parsing results"); - -// parse("%A", "Tuesday", &dt)!; -// assert(dt.weekday as int == 2, "invalid parsing results"); - -// parse("%b", "Feb", &dt)!; -// assert(dt.month as int == 2, "invalid parsing results"); - -// parse("%B", "February", &dt)!; -// assert(dt.month as int == 2, "invalid parsing results"); - -// parse("%I", "14", &dt)!; -// assert(dt.hour as int == 2, "invalid parsing results"); - -// parse("%j", "123", &dt)!; -// assert(dt.yearday as int == 123, "invalid parsing results"); - -// parse("%H %p", "6 AM", &dt)!; -// assert(dt.hour as int == 6, "invalid parsing results"); - -// parse("%H %p", "6 PM", &dt)!; -// assert(dt.hour as int == 18, "invalid parsing results"); - -// assert(parse("%H %p", "13 PM", &dt) is invalid, -// "invalid parsing results"); - -// assert(parse("%H %p", "PM 6", &dt) is invalid, -// "invalid parsing results"); - -// parse("%u", "7", &dt)!; -// assert(dt.weekday as int == 7, "invalid parsing results"); - -// parse("%U", "2", &dt)!; -// assert(dt.week as int == 2, "invalid parsing results"); - -// parse("%U", "99", &dt)!; -// assert(dt.week as int == 53, "invalid parsing results"); - -// parse("%w", "0", &dt)!; -// assert(dt.weekday as int == 7, "invalid parsing results"); - -// parse("%W", "2", &dt)!; -// assert(dt.week as int == 2, "invalid parsing results"); -// }; diff --git a/datetime/parse.ha b/datetime/parse.ha @@ -2,175 +2,403 @@ // (c) 2021-2022 Byron Torres <b@torresjrjr.com> // (c) 2022 Drew DeVault <sir@cmpwn.com> // (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net> +use ascii; use errors; +use io; +use strconv; use strings; +use strio; use time; use time::chrono; -// Parses a date/time string into a [[builder]], according to a layout format +type failure = !void; + +// A parsing error occurred. If appropriate, the offending format specifier is +// stored. A null rune represents all other error cases. +export type parsefail = !rune; + +// Parses a date/time string into a [[virtual]], according to a layout format // string with specifiers as documented under [[format]]. Partial, sequential, // aggregative parsing is possible. // -// datetime::parse(&builder, "%Y-%m-%d", "2038-01-19"); -// datetime::parse(&builder, "%H:%M:%S", "03:14:07"); +// datetime::parse(&v, "%Y-%m-%d", "2019-12-27"); +// datetime::parse(&v, "%H:%M:%S.%N", "22:07:08.000000000"); +// datetime::parse(&v, "%z %Z %L", "+0100 CET Europe/Amsterdam"); // -export fn parse(build: *builder, layout: str, s: str) (void | invalid) = { - const format_iter = strings::iter(layout); - const s_iter = strings::iter(s); +export fn parse(v: *virtual, layout: str, s: str) (void | parsefail) = { + const liter = strings::iter(layout); + const siter = strings::iter(s); let escaped = false; + for (true) { - let format_r: rune = match (strings::next(&format_iter)) { + const lr: rune = match (strings::next(&liter)) { case void => break; - case let r: rune => - yield r; + case let lr: rune => + yield lr; }; - if (!escaped && format_r == '%') { + if (!escaped && lr == '%') { escaped = true; continue; }; if (!escaped) { - let s_r = match (strings::next(&s_iter)) { + const sr = match (strings::next(&siter)) { case void => - return invalid; - case let r: rune => - yield r; + return '\x00'; + case let sr: rune => + yield sr; }; - if (s_r != format_r) { - return invalid; + if (sr != lr) { + return '\x00'; }; continue; }; escaped = false; - switch (format_r) { - // Basic specifiers - case 'a' => - build.weekday = get_default_locale_string_index( - &s_iter, WEEKDAYS_SHORT[..])?; - case 'A' => - build.weekday = get_default_locale_string_index( - &s_iter, WEEKDAYS[..])?; - case 'b' => - build.month = get_default_locale_string_index( - &s_iter, MONTHS_SHORT[..])?; - case 'B' => - build.month = get_default_locale_string_index( - &s_iter, MONTHS[..])?; - case 'd' => - let max_n_digits = 2u; - build.day = clamp_int( - get_max_n_digits(&s_iter, max_n_digits)?, 1, 31); - case 'H' => - let max_n_digits = 2u; - build.hour = clamp_int( - get_max_n_digits(&s_iter, max_n_digits)?, 0, 23); - case 'I' => - let max_n_digits = 2u; - const hour = get_max_n_digits(&s_iter, max_n_digits); - build.hour = match (hour) { - case let hour: int => - yield if (hour > 12) { - yield clamp_int(hour - 12, 1, 12); - } else { - yield clamp_int(hour, 1, 12); - }; - case => - return invalid; - }; - case 'j' => - build.yearday = clamp_int( - get_max_n_digits(&s_iter, 3)?, 1, 366); - case 'L' => - // TODO: Parse %L (locality/timezone name/ID). - continue; - case 'm' => - build.month = clamp_int( - get_max_n_digits(&s_iter, 2)?, 1, 12); - case 'M' => - build.minute = clamp_int( - get_max_n_digits(&s_iter, 2)?, 0, 59); - case 'N' => - build.nanosecond = clamp_int( - get_max_n_digits(&s_iter, 9)?, 0, 999999999); - case 'p' => - if (build.hour is void) { - // We can't change the hour's am/pm because we - // have no hour. - return invalid; - }; - const rest = strings::iterstr(&s_iter); - if (strings::hasprefix(rest, "AM")) { - if (build.hour as int > 12) { - // 13 AM? - return invalid; - } else if (build.hour as int == 12) { - build.hour = 0; - }; - } else if (strings::hasprefix(rest, "PM")) { - if (build.hour as int > 12) { - // 13 PM? - return invalid; - } else if (build.hour as int < 12) { - build.hour = - (build.hour as int) + 12; - }; - } else { - return invalid; - }; - strings::next(&s_iter); - strings::next(&s_iter); - case 'S' => - build.second = clamp_int( - get_max_n_digits(&s_iter, 2)?, 0, 61); - case 'u', 'w' => - build.weekday = match (get_max_n_digits(&s_iter, 1)) { - case let i: int => - yield if (format_r == 'w') { - yield if (i == 0) { - yield 7; - } else { - yield clamp_int(i, 1, 7); - }; - } else { - yield clamp_int(i, 1, 7); - }; - case => - return invalid; - }; - case 'U', 'W' => - build.week = clamp_int( - get_max_n_digits(&s_iter, 2)?, 0, 53); - case 'Y' => - build.year = get_max_n_digits(&s_iter, 4)?; - case 'z' => - const rest = strings::iterstr(&s_iter); - if(strings::hasprefix(rest, 'Z') || strings::hasprefix(rest, 'z')) { - (build.zone: chrono::zone).zoffset = 0; - } else { - const prefix = strings::next(&s_iter); - (build.zone: chrono::zone).zoffset = get_max_n_digits(&s_iter, 2)? * time::HOUR; - - const rest = strings::iterstr(&s_iter); - if(strings::hasprefix(rest, ":")) { - strings::next(&s_iter); - }; - - (build.zone: chrono::zone).zoffset += get_max_n_digits(&s_iter, 2)? * time::MINUTE; - - if(prefix == '-') { - (build.zone: chrono::zone).zoffset *= -1; - }; + + match (parse_specifier(v, &siter, lr)) { + case void => void; + case failure => + return lr; + }; + }; + + return void; +}; + +fn parse_specifier( + v: *virtual, + iter: *strings::iterator, + lr: rune, +) (void | failure) = { + switch (lr) { + case 'a' => v.weekday = + scan_for(iter, WEEKDAYS_SHORT...)?; + case 'A' => v.weekday = + scan_for(iter, WEEKDAYS...)?; + case 'b' => v.month = + scan_for(iter, MONTHS_SHORT...)? + 1; + case 'B' => v.month = + scan_for(iter, MONTHS...)? + 1; + case 'd' => v.day = + scan_int(iter, 2, false)?; + case 'H' => v.hour = + scan_int(iter, 2, false)?; + case 'I' => v.halfhour = + scan_int(iter, 2, false)?; + case 'j' => v.yearday = + scan_int(iter, 3, false)?; + case 'L' => v.locname = + scan_str(iter)?; + case 'm' => v.month = + scan_int(iter, 2, false)?; + case 'M' => v.minute = + scan_int(iter, 2, false)?; + case 'N' => v.nanosecond = + scan_int(iter, 9, true)?; + case 'p' => v.ampm = // AM=false PM=true + scan_for(iter, "AM", "PM", "am", "pm")? % 2 == 1; + case 'S' => v.second = + scan_int(iter, 2, false)?; + case 'u' => v.weekday = + scan_int(iter, 1, false)? - 1; + case 'U' => v.week = + scan_int(iter, 2, false)?; + case 'w' => v.weekday = + scan_int(iter, 1, false)? - 1; + case 'W' => v.week = + scan_int(iter, 2, false)?; + case 'Y' => v.year = + scan_int(iter, 4, false)?; + case 'z' => v.zoff = + scan_zo(iter)?; + case 'Z' => v.zabbr = + scan_str(iter)?; + case '%' => + eat_rune(iter, '%')?; + case => + void; // Ignore invalid specifier + }; +}; + +fn eat_rune(iter: *strings::iterator, needle: rune) (uint | failure) = { + const rn = match (strings::next(iter)) { + case void => + return failure; + case let rn: rune => + yield rn; + }; + if (rn == needle) { + return 1; + } else { + strings::prev(iter); + return 0; + }; +}; + +// Scans the iterator for a given list of strings. +// Returns the list index of the matched string. +fn scan_for(iter: *strings::iterator, list: str...) (int | failure) = { + const name = strings::iterstr(iter); + if (len(name) == 0) { + return failure; + }; + for(let i = 0z; i < len(list); i += 1) { + if (strings::hasprefix(name, list[i])) { + // Consume name + for (let j = 0z; j < len(list[i]); j += 1) { + strings::next(iter); }; - case '%' => - eat_one_rune(&s_iter, '%')?; + return i: int; + }; + }; + return failure; +}; +// Scans the iterator upto n consecutive numeric digits. +// Returns the resulting int. +// If pad is true, the number is right-padded with zeroes upto n digits. +fn scan_int(iter: *strings::iterator, n: size, pad: bool) (int | failure) = { + let buf: [64]u8 = [0...]; + let bufstr = strio::fixed(buf); + for (let i = 0z; i < n; i += 1) { + let rn: rune = match (strings::next(iter)) { + case void => + break; + case let rn: rune => + yield rn; + }; + if (!ascii::isdigit(rn)) { + strings::prev(iter); + break; + }; + match (strio::appendrune(&bufstr, rn)) { + case io::error => + return failure; case => - // Ignore invalid specifier - continue; + void; }; }; - return void; + const s = strings::padend(strio::string(&bufstr), '0', if (pad) n else 0); + defer free(s); + + match (strconv::stoi(s)) { + case let n: int => + return n; + case => + return failure; + }; +}; + +// Scans and parses zone offsets of the form: +// +// Z +// z +// +nn:nn +// -nn:nn +// +fn scan_zo(iter: *strings::iterator) (time::duration | failure) = { + const rest = strings::iterstr(iter); + if (strings::hasprefix(rest, 'Z') || strings::hasprefix(rest, 'z')) { + return 0; + } else { + const prefix = strings::next(iter); + let zo = scan_int(iter, 2, false)? * time::HOUR; + const rest = strings::iterstr(iter); + if (strings::hasprefix(rest, ":")) { + strings::next(iter); + }; + zo += scan_int(iter, 2, false)? * time::MINUTE; + if (prefix == '-') { + zo *= -1; + }; + return zo; + }; +}; + +// Scans and parses locality names, made of printable characters. +fn scan_str(iter: *strings::iterator) (str | failure) = { + static let buf: [64]u8 = [0...]; + let bufstr = strio::fixed(buf); + for (true) { + let rn: rune = match (strings::next(iter)) { + case void => + break; + case let rn: rune => + yield rn; + }; + if (!ascii::isgraph(rn)) { + strings::prev(iter); + break; + }; + strio::appendrune(&bufstr, rn)!; + }; + return strio::string(&bufstr); +}; + +@test fn parse() void = { + let v = newvirtual(); + assert(parse(&v, "foo", "foo") is void, "none: parsefail"); + assert(v.zone == null, "none: non-null zone"); + assert(v.date is void, "none: non-void date"); + assert(v.time is void, "none: non-void time"); + assert(v.era is void, "none: non-void era"); + assert(v.year is void, "none: non-void year"); + assert(v.month is void, "none: non-void month"); + assert(v.day is void, "none: non-void day"); + assert(v.yearday is void, "none: non-void yearday"); + assert(v.isoweekyear is void, "none: non-void isoweekyear"); + assert(v.isoweek is void, "none: non-void isoweek"); + assert(v.week is void, "none: non-void week"); + assert(v.sundayweek is void, "none: non-void sundayweek"); + assert(v.weekday is void, "none: non-void weekday"); + assert(v.hour is void, "none: non-void hour"); + assert(v.minute is void, "none: non-void minute"); + assert(v.second is void, "none: non-void second"); + assert(v.nanosecond is void, "none: non-void nanosecond"); + assert(v.vloc is void, "none: non-void vloc"); + assert(v.locname is void, "none: non-void locname"); + assert(v.zoff is void, "none: non-void zoff"); + assert(v.zabbr is void, "none: non-void zabbr"); + assert(v.halfhour is void, "none: non-void halfhour"); + assert(v.ampm is void, "none: non-void ampm"); + + let v = newvirtual(); + assert(parse(&v, "%a", "Fri") is void , "%a: parsefail"); + assert(v.weekday is int , "%a: void"); + assert(v.weekday as int == 4 , "%a: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%A", "Friday") is void , "%A: parsefail"); + assert(v.weekday is int , "%A: void"); + assert(v.weekday as int == 4 , "%A: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%b", "Jan") is void , "%b: parsefail"); + assert(v.month is int , "%b: void"); + assert(v.month as int == 1 , "%b: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%B", "January") is void , "%B: parsefail"); + assert(v.month is int , "%B: void"); + assert(v.month as int == 1 , "%B: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%d", "27") is void , "%d: parsefail"); + assert(v.day is int , "%d: void"); + assert(v.day as int == 27 , "%d: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%H", "22") is void , "%H: parsefail"); + assert(v.hour is int , "%H: void"); + assert(v.hour as int == 22 , "%H: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%I", "10") is void , "%I: parsefail"); + assert(v.halfhour is int , "%I: void"); + assert(v.halfhour as int == 10 , "%I: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%j", "361") is void , "%j: parsefail"); + assert(v.yearday is int , "%j: void"); + assert(v.yearday as int == 361 , "%j: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%L", "Europe/Amsterdam") is void , "%L: parsefail"); + assert(v.locname is str , "%L: void"); + assert(v.locname as str == "Europe/Amsterdam" , "%L: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%m", "12") is void , "%m: parsefail"); + assert(v.month is int , "%m: void"); + assert(v.month as int == 12 , "%m: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%M", "07") is void , "%M: parsefail"); + assert(v.minute is int , "%M: void"); + assert(v.minute as int == 7 , "%M: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%N", "123456789") is void , "%N: parsefail"); + assert(v.nanosecond is int , "%N: void"); + assert(v.nanosecond as int == 123456789 , "%N: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%p", "PM") is void , "%p: parsefail"); + assert(v.ampm is bool , "%p: void"); + assert(v.ampm as bool == true , "%p: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%S", "08") is void , "%S: parsefail"); + assert(v.second is int , "%S: void"); + assert(v.second as int == 8 , "%S: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%u", "5") is void , "%u: parsefail"); + assert(v.weekday is int , "%u: void"); + assert(v.weekday as int == 4 , "%u: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%U", "51") is void , "%U: parsefail"); + assert(v.week is int , "%U: void"); + assert(v.week as int == 51 , "%U: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%w", "5") is void , "%w: parsefail"); + assert(v.weekday is int , "%w: void"); + assert(v.weekday as int == 4 , "%w: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%W", "51") is void , "%W: parsefail"); + assert(v.week is int , "%W: void"); + assert(v.week as int == 51 , "%W: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%Y", "2019") is void , "%Y: parsefail"); + assert(v.year is int , "%Y: void"); + assert(v.year as int == 2019 , "%Y: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%z", "+0100") is void , "%z: parsefail"); + assert(v.zoff is i64 , "%z: void"); + assert(v.zoff as i64 == 1 * time::HOUR , "%z: incorrect"); + + let v = newvirtual(); + assert(parse(&v, "%Z", "CET") is void , "%Z: parsefail"); + assert(v.zabbr is str , "%Z: void"); + assert(v.zabbr as str == "CET" , "%Z: incorrect"); + + let v = newvirtual(); + assert(( + parse(&v, + "%Y-%m-%d %H:%M:%S.%N %z %Z %L", + "2038-01-19 03:14:07.000000000 +0000 UTC UTC", + ) + is void + ), + "test 1: parsefail" + ); + assert(v.year is int , "test 1: year void"); + assert(v.year as int == 2038, "test 1: year incorrect"); + assert(v.month is int , "test 1: month void"); + assert(v.month as int == 1, "test 1: month incorrect"); + assert(v.day is int , "test 1: day void"); + assert(v.day as int == 19, "test 1: day incorrect"); + assert(v.hour is int , "test 1: hour void"); + assert(v.hour as int == 3, "test 1: hour incorrect"); + assert(v.minute is int , "test 1: minute void"); + assert(v.minute as int == 14, "test 1: minute incorrect"); + assert(v.second is int , "test 1: second void"); + assert(v.second as int == 7, "test 1: second incorrect"); + assert(v.nanosecond is int , "test 1: nanosecond void"); + assert(v.nanosecond as int == 0, "test 1: nanosecond incorrect"); + assert(v.zoff is i64 , "test 1: zoff void"); + assert(v.zoff as i64 == 0, "test 1: zoff incorrect"); + assert(v.zabbr is str , "test 1: zabbr void"); + assert(v.zabbr as str == "UTC", "test 1: zabbr incorrect"); + assert(v.locname is str , "test 1: locname void"); + assert(v.locname as str == "UTC", "test 1: locname incorrect"); + }; diff --git a/datetime/period.ha b/datetime/period.ha @@ -0,0 +1,69 @@ +// License: MPL-2.0 +// (c) 2023 Byron Torres <b@torresjrjr.com> + +// Represents a span of time in the Gregorian chronology, using nominal units of +// time. Used for chronological arithmetic. +export type period = struct { + years: i64, + months: i64, + weeks: i64, + days: i64, + hours: i64, + minutes: i64, + seconds: i64, + nanoseconds: i64, +}; + +// Returns true if two [[period]]s are numerically equal, false otherwise. +export fn peq(pa: period, pb: period) bool = { + return ( + pa.years == pb.years + && pa.months == pb.months + && pa.weeks == pb.weeks + && pa.days == pb.days + && pa.hours == pb.hours + && pa.minutes == pb.minutes + && pa.seconds == pb.seconds + && pa.nanoseconds == pb.nanoseconds + ); +}; + +// Returns the sum [[period]] of a set of periods. +export fn sum(ps: period...) period = { + let p = period { ... }; + for (let i = 0z; i < len(ps); i += 1) { + p.years += ps[i].years; + p.months += ps[i].months; + p.weeks += ps[i].weeks; + p.days += ps[i].days; + p.hours += ps[i].hours; + p.minutes += ps[i].minutes; + p.seconds += ps[i].seconds; + p.nanoseconds += ps[i].nanoseconds; + }; + return p; +}; + +// Returns a [[period]] with its fields negated. +export fn neg(p: period) period = period { + years = -p.years, + months = -p.months, + weeks = -p.weeks, + days = -p.days, + hours = -p.hours, + minutes = -p.minutes, + seconds = -p.seconds, + nanoseconds = -p.nanoseconds, +}; + +// Returns a [[period]] with its fields made absolute and positive. +export fn abs(p: period) period = period { + years = if (p.years < 0) -p.years else p.years, + months = if (p.months < 0) -p.months else p.months, + weeks = if (p.weeks < 0) -p.weeks else p.weeks, + days = if (p.days < 0) -p.days else p.days, + hours = if (p.hours < 0) -p.hours else p.hours, + minutes = if (p.minutes < 0) -p.minutes else p.minutes, + seconds = if (p.seconds < 0) -p.seconds else p.seconds, + nanoseconds = if (p.nanoseconds < 0) -p.nanoseconds else p.nanoseconds, +}; diff --git a/datetime/reckon.ha b/datetime/reckon.ha @@ -0,0 +1,489 @@ +// License: MPL-2.0 +// (c) 2023 Byron Torres <b@torresjrjr.com> +use time; +use time::chrono; + +// Specifies the behaviour of [[reckon]] when doing chronological arithmetic. +// +// The FLOOR, CEIL, HOP, and FOLD specifies how to resolve sub-significant +// overflows -- when a field's change in value causes any sub-significant +// field's range to shrink below its current value and become invalid. For +// example, adding 1 month to January 31st results in February 31st, a date with +// an unresolved day field, since February permits only 28 or 29 days. +export type calculus = enum uint { + // The default behaviour. Equivalent to CEIL. + DEFAULT = 0, + + // Apply units in reverse order, from least to most significant. + REVSIG = 1 << 0, + + // When a sub-significant overflow occurs, the unresolved field is set + // to its minimum valid value. + // + // Feb 31 -> Feb 01 + // Aug 64 -> Aug 01 + FLOOR = 1 << 1, + + // When a sub-significant overflow occurs, the unresolved field is set + // to its maximum valid value. + // + // Feb 31 -> Feb 28 / Feb 29 (leap year dependent) + // Aug 64 -> Aug 31 + CEIL = 1 << 2, + + // When a sub-significant overflow occurs, the unresolved field is set + // to its new minimum valid value after the next super-significant field + // increments by one. + // + // Feb 31 -> Mar 01 + // Aug 64 -> Sep 01 + HOP = 1 << 3, + + // When a sub-significant overflow occurs, the unresolved field's + // maximum valid value is subtracted from it's current value, and the + // next super-significant field increments by one. This process repeats + // until the unresolved field's value becomes valid (falls in range). + // + // Feb 31 -> Mar 03 / Mar 02 (leap year dependent) + // Aug 64 -> Sep 33 -> Oct 03 + FOLD = 1 << 4, +}; + +// Reckons from a given [[datetime]] to a new one, via a set of [[period]]s. +// This is a chronology-wise arithmetic operation. Each period is reckoned +// independently in succession, applying (adding) their units from most to least +// significant. +// +// The [[calculus]] parameter determines arithmetic and resolution behaviour +// when encountering deviations (e.g. overflows). +// +// let dest = datetime::reckon( +// start, // 2000-02-29 09:00:00 +// 0, // calculus::DEFAULT +// datetime::period { +// years = 1, // becomes: 2001-02-28 09:00:00 +// months = -2, // becomes: 2000-12-28 09:00:00 +// days = 4, // becomes: 2001-01-01 09:00:00 +// }, +// ); +// +// See [[add]] for a timescale-wise arithmetic operation which uses +// [[time::duration]]. +export fn reckon(dt: datetime, calc: calculus, ps: period...) datetime = { + let r = newvirtual(); // our reckoner + r.vloc = dt.loc; + r.zoff = chrono::mzone(&dt).zoff; + r.year = _year(&dt); + r.month = _month(&dt); + r.day = _day(&dt); + r.hour = _hour(&dt); + r.minute = _minute(&dt); + r.second = _second(&dt); + r.nanosecond = _nanosecond(&dt); + + if (calc == calculus::DEFAULT) { + calc |= calculus::CEIL; + }; + + for (let i = 0z; i < len(ps); i += 1) if (calc & calculus::REVSIG == 0) { + const p = ps[i]; + const fold = calculus::FOLD; + + r.year = r.year as int + p.years: int; + reckon_days(&r, 0, calc); // bubble up potential Feb 29 overflow + + reckon_months(&r, p.months); + reckon_days(&r, 0, calc); // bubble up potential overflows + + reckon_days(&r, p.weeks * 7, fold); + reckon_days(&r, p.days, fold); + + // TODO: These functions aren't aware of top-down overflows. + // Handle overflows (e.g. [[zone]] changes). + reckon_hours(&r, p.hours, fold); + reckon_minutes(&r, p.minutes, fold); + reckon_seconds(&r, p.seconds, fold); + reckon_nanoseconds(&r, p.nanoseconds, fold); + } else { + const p = ps[i]; + const fold = calculus::FOLD | calculus::REVSIG; + + reckon_nanoseconds(&r, p.nanoseconds, fold); + reckon_seconds(&r, p.seconds, fold); + reckon_minutes(&r, p.minutes, fold); + reckon_hours(&r, p.hours, fold); + reckon_days(&r, p.days, fold); + reckon_days(&r, p.weeks * 7, fold); + + reckon_months(&r, p.months); + reckon_days(&r, 0, calc); // bubble up potential overflows + + r.year = r.year as int + p.years: int; + reckon_days(&r, 0, calc); // bubble up potential Feb 29 overflow + }; + + return realize(r)!; +}; + +fn reckon_months(r: *virtual, months: i64) void = { + let year = r.year as int; + let month = r.month as int; + + month += months: int; + + // month overflow + for (month > 12) { + month -= 12; + year += 1; + }; + for (month < 1) { + month += 12; + year -= 1; + }; + + r.year = year; + r.month = month; +}; + +fn reckon_days(r: *virtual, days: i64, calc: calculus) void = { + let year = r.year as int; + let month = r.month as int; + let day = r.day as int; + + day += days: int; + + // day overflow + let month_daycnt = calc_month_daycnt(year, month); + for (day > month_daycnt) { + if (calc & calculus::FLOOR != 0) { + day = 1; + } else if (calc & calculus::CEIL != 0) { + day = month_daycnt; + } else if (calc & calculus::HOP != 0) { + r.year = year; + r.month = month; + + reckon_months(r, 1); + + year = r.year as int; + month = r.month as int; + day = 1; + } else if (calc & calculus::FOLD != 0) { + r.year = year; + r.month = month; + + reckon_months(r, 1); + + year = r.year as int; + month = r.month as int; + day -= month_daycnt; + }; + month_daycnt = calc_month_daycnt(year, month); + }; + for (day < 1) { + r.year = year; + r.month = month; + + reckon_months(r, -1); + + year = r.year as int; + month = r.month as int; + day += calc_month_daycnt(year, month); + }; + + r.year = year; + r.month = month; + r.day = day; +}; + +fn reckon_hours(r: *virtual, hours: i64, calc: calculus) void = { + let hour = r.hour as int; + + hour += hours: int; + + // hour overflow + for (hour >= 24) { + reckon_days(r, 1, calc); + hour -= 24; + }; + for (hour < 0) { + reckon_days(r, -1, calc); + hour += 24; + }; + + r.hour = hour; +}; + +fn reckon_minutes(r: *virtual, mins: i64, calc: calculus) void = { + let min = r.minute as int; + + min += mins: int; + + // minute overflow + for (min >= 60) { + reckon_hours(r, 1, calc); + min -= 60; + }; + for (min < 0) { + reckon_hours(r, -1, calc); + min += 60; + }; + + r.minute = min; +}; + +fn reckon_seconds(r: *virtual, secs: i64, calc: calculus) void = { + let s = r.second as int; + + s += secs: int; + + // second overflow + for (s >= 60) { + reckon_minutes(r, 1, calc); + s -= 60; + }; + for (s < 0) { + reckon_minutes(r, -1, calc); + s += 60; + }; + + r.second = s; +}; + +fn reckon_nanoseconds(r: *virtual, nsecs: i64, calc: calculus) void = { + let ns = r.nanosecond as int; + + ns += nsecs: int; + + // nanosecond overflow + for (ns >= 1000000000) { // 1E9 nanoseconds (1 second) + reckon_seconds(r, 1, calc); + ns -= 1000000000; + }; + for (ns < 0) { + reckon_seconds(r, -1, calc); + ns += 1000000000; + }; + + r.nanosecond = ns; +}; + +@test fn reckon() void = { + const Amst = chrono::tz("Europe/Amsterdam")!; + defer chrono::timezone_free(Amst); + + // no-op period, calculus::CEIL + + let p = period { ... }; + + let a = new(chrono::UTC, 0)!; + let r = reckon(a, 0, p); + assert(chrono::eq(&a, &r)!, "01. incorrect result"); + + let a = new(chrono::UTC, 0, 2019, 12, 27, 21, 7, 8, 0)!; + let r = reckon(a, 0, p); + assert(chrono::eq(&a, &r)!, "02. incorrect result"); + + let a = new(Amst, 1 * time::HOUR, 2019, 12, 27, 22, 7, 8, 0)!; + let r = reckon(a, 0, p); + assert(chrono::eq(&a, &r)!, "03. incorrect result"); + + // generic periods, calculus::CEIL + + let a = new(chrono::UTC, 0, 2019, 12, 27, 21, 7, 8, 0)!; + + let r = reckon(a, 0, period { + years = 1, + months = 1, + days = 1, + hours = 1, + minutes = 1, + seconds = 1, + nanoseconds = 1, + ... + }); + let b = new(chrono::UTC, 0, 2021, 1, 28, 22, 8, 9, 1)!; + assert(chrono::eq(&b, &r)!, "04. incorrect result"); + + let r = reckon(a, 0, period { + years = -1, + months = -1, + days = -1, + hours = -1, + minutes = -1, + seconds = -1, + nanoseconds = -1, + ... + }); + let b = new(chrono::UTC, 0, 2018, 11, 26, 20, 6, 6, 999999999)!; + assert(chrono::eq(&b, &r)!, "05. incorrect result"); + + let r = reckon(a, 0, period { + years = 100, + months = 100, + days = 100, + hours = 100, + minutes = 100, + seconds = 100, + nanoseconds = 100, + ... + }); + let b = new(chrono::UTC, 0, 2128, 8, 10, 2, 48, 48, 100)!; + assert(chrono::eq(&b, &r)!, "06. incorrect result"); + + let r = reckon(a, 0, period { + years = -100, + months = -100, + days = -100, + hours = -100, + minutes = -100, + seconds = -100, + nanoseconds = -100, + ... + }); + let b = new(chrono::UTC, 0, 1911, 5, 15, 15, 25, 27, 999999900)!; + assert(chrono::eq(&b, &r)!, "07. incorrect result"); + + let r = reckon(a, 0, period { + weeks = 100, + ... + }); + let b = new(chrono::UTC, 0, 2021, 11, 26, 21, 7, 8, 0)!; + assert(chrono::eq(&b, &r)!, "08. incorrect result"); + + // calculus, February 29 overflows + + let a = new(chrono::UTC, 0, 2000, 1, 31)!; // leap year + let p = period { months = 1, ... }; + + let r = reckon(a, calculus::FLOOR, p); + let b = new(chrono::UTC, 0, 2000, 2, 1)!; + assert(chrono::eq(&b, &r)!, "09. incorrect result"); + + let r = reckon(a, calculus::CEIL, p); + let b = new(chrono::UTC, 0, 2000, 2, 29)!; + assert(chrono::eq(&b, &r)!, "10. incorrect result"); + + let r = reckon(a, calculus::HOP, p); + let b = new(chrono::UTC, 0, 2000, 3, 1)!; + assert(chrono::eq(&b, &r)!, "11. incorrect result"); + + let r = reckon(a, calculus::FOLD, p); + let b = new(chrono::UTC, 0, 2000, 3, 2)!; + assert(chrono::eq(&b, &r)!, "12. incorrect result"); + + // calculus, February 28 overflows + + let a = new(chrono::UTC, 0, 2000, 1, 31)!; // leap year + let p = period { years = 1, months = 1, ... }; + + let r = reckon(a, calculus::FLOOR, p); + let b = new(chrono::UTC, 0, 2001, 2, 1)!; + assert(chrono::eq(&b, &r)!, "13. incorrect result"); + + let r = reckon(a, calculus::CEIL, p); + let b = new(chrono::UTC, 0, 2001, 2, 28)!; + assert(chrono::eq(&b, &r)!, "14. incorrect result"); + + let r = reckon(a, calculus::HOP, p); + let b = new(chrono::UTC, 0, 2001, 3, 1)!; + assert(chrono::eq(&b, &r)!, "15. incorrect result"); + + let r = reckon(a, calculus::FOLD, p); + let b = new(chrono::UTC, 0, 2001, 3, 3)!; + assert(chrono::eq(&b, &r)!, "16. incorrect result"); + + // multiple periods + + let a = new(chrono::UTC, 0, 2000, 12, 31)!; + let ps = [ + period { years = +1, months = +1, days = +1, ... }, + period { years = -1, months = -1, days = -1, ... }, + period { years = -1, months = -1, days = -1, ... }, + period { years = +1, months = +1, days = +1, ... }, + period { hours = +1, minutes = +1, seconds = +1, ... }, + period { hours = -1, minutes = -1, seconds = -1, ... }, + period { hours = -1, minutes = -1, seconds = -1, ... }, + period { hours = +1, minutes = +1, seconds = +1, ... }, + ]; + + let r = reckon(a, 0, ps[..1]...); + let b = new(chrono::UTC, 0, 2002, 2, 1)!; + assert(chrono::eq(&b, &r)!, "17. incorrect result"); + + let r = reckon(a, 0, ps[..2]...); + let b = new(chrono::UTC, 0, 2000, 12, 31)!; + assert(chrono::eq(&b, &r)!, "18. incorrect result"); + + let r = reckon(a, 0, ps[..3]...); + let b = new(chrono::UTC, 0, 1999, 11, 29)!; + assert(chrono::eq(&b, &r)!, "19. incorrect result"); + + let r = reckon(a, 0, ps[..4]...); + let b = new(chrono::UTC, 0, 2000, 12, 30)!; + assert(chrono::eq(&b, &r)!, "20. incorrect result"); + + let r = reckon(a, 0, ps[..5]...); + let b = new(chrono::UTC, 0, 2000, 12, 30, 1, 1, 1)!; + assert(chrono::eq(&b, &r)!, "21. incorrect result"); + + let r = reckon(a, 0, ps[..6]...); + let b = new(chrono::UTC, 0, 2000, 12, 30)!; + assert(chrono::eq(&b, &r)!, "22. incorrect result"); + + let r = reckon(a, 0, ps[..7]...); + let b = new(chrono::UTC, 0, 2000, 12, 29, 22, 58, 59)!; + assert(chrono::eq(&b, &r)!, "23. incorrect result"); + + let r = reckon(a, 0, ps[..8]...); + let b = new(chrono::UTC, 0, 2000, 12, 30)!; + assert(chrono::eq(&b, &r)!, "24. incorrect result"); + + // multiple periods, calculus::REVSIG + + let a = new(chrono::UTC, 0, 2000, 12, 31)!; + let ps = [ + period { years = +1, months = +1, days = +1, ... }, + period { years = -1, months = -1, days = -1, ... }, + period { years = -1, months = -1, days = -1, ... }, + period { years = +1, months = +1, days = +1, ... }, + period { hours = +1, minutes = +1, seconds = +1, ... }, + period { hours = -1, minutes = -1, seconds = -1, ... }, + period { hours = -1, minutes = -1, seconds = -1, ... }, + period { hours = +1, minutes = +1, seconds = +1, ... }, + ]; + + let r = reckon(a, calculus::REVSIG, ps[..1]...); + let b = new(chrono::UTC, 0, 2002, 2, 1)!; + assert(chrono::eq(&b, &r)!, "25. incorrect result"); + + let r = reckon(a, calculus::REVSIG, ps[..2]...); + let b = new(chrono::UTC, 0, 2000, 12, 31)!; + assert(chrono::eq(&b, &r)!, "26. incorrect result"); + + let r = reckon(a, calculus::REVSIG, ps[..3]...); + let b = new(chrono::UTC, 0, 1999, 11, 30)!; + assert(chrono::eq(&b, &r)!, "27. incorrect result"); + + let r = reckon(a, calculus::REVSIG, ps[..4]...); + let b = new(chrono::UTC, 0, 2001, 1, 1)!; + assert(chrono::eq(&b, &r)!, "28. incorrect result"); + + let r = reckon(a, calculus::REVSIG, ps[..5]...); + let b = new(chrono::UTC, 0, 2001, 1, 1, 1, 1, 1)!; + assert(chrono::eq(&b, &r)!, "29. incorrect result"); + + let r = reckon(a, calculus::REVSIG, ps[..6]...); + let b = new(chrono::UTC, 0, 2001, 1, 1)!; + assert(chrono::eq(&b, &r)!, "30. incorrect result"); + + let r = reckon(a, calculus::REVSIG, ps[..7]...); + let b = new(chrono::UTC, 0, 2000, 12, 31, 22, 58, 59)!; + assert(chrono::eq(&b, &r)!, "31. incorrect result"); + + let r = reckon(a, calculus::REVSIG, ps[..8]...); + let b = new(chrono::UTC, 0, 2001, 1, 1)!; + assert(chrono::eq(&b, &r)!, "32. incorrect result"); + + return; +}; diff --git a/datetime/time.ha b/datetime/time.ha @@ -16,7 +16,7 @@ fn calc_hmsn(t: time::duration) (int, int, int, int) = { // Calculates the time since the start of a day, // given a wall clock (hour, minute, second, nanosecond). -fn calc_time_from_hmsn( +fn calc_time__hmsn( hour: int, min: int, sec: int, diff --git a/datetime/timezone.ha b/datetime/timezone.ha @@ -6,6 +6,8 @@ use time::chrono; // Creates an equivalent [[datetime]] with a different // [[time::chrono::locality]]. -export fn in(loc: chrono::locality, dt: datetime) datetime = { - return from_moment(chrono::in(loc, *(&dt: *chrono::moment))); +// +// The [[discontinuity]] rules from [[time::chrono::in]] apply here. +export fn in(loc: chrono::locality, dt: datetime) (datetime | chrono::discontinuity) = { + return from_moment(chrono::in(loc, *(&dt: *chrono::moment))?); }; diff --git a/datetime/virtual.ha b/datetime/virtual.ha @@ -0,0 +1,225 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use time; +use time::chrono; + +// A [[virtual]] has insufficient information and cannot create a valid datetime. +export type insufficient = !void; + +// A virtual datetime; a [[datetime]] wrapper interface, which represents a +// datetime of uncertain validity. Its fields need not be valid observed +// chronological values. It is meant as an intermediary container for datetime +// information to be resolved with the [[realize]] function. +// +// Unlike [[datetime]], a virtual's fields are meant to be treated as public and +// mutable. The embedded [[time::instant]] and [[time::chrono::locality]] fields +// (.sec .nsec .loc) are considered meaningless. Behaviour with the "observe" +// functions is undefined. +// +// This can be used to construct a new [[datetime]] piece-by-piece. Start with +// [[newvirtual]], then collect enough date/time information incrementally by +// direct field assignments and/or with [[parse]]. Finish with [[realize]]. +// +// let v = datetime::newvirtual(); +// v.vloc = time::chrono::UTC; +// v.zoff = 0; +// datetime::parse(&v, "Date: %Y-%m-%d", "Date: 2038-01-19")!; +// v.hour = 03; +// v.minute = 14; +// v.second = 07; +// v.nanosecond = 0; +// let dt = datetime::realize(v)!; +// +export type virtual = struct { + datetime, + // virtual's locality + vloc: (void | time::chrono::locality), + // locality name + locname: (void | str), + // zone offset + zoff: (void | time::duration), + // zone abbreviation + zabbr: (void | str), + // hour of 12 hour clock + halfhour: (void | int), + // AM/PM (false/true) + ampm: (void | bool), +}; + +// Creates a new [[virtual]]. All its fields are voided or nulled appropriately. +export fn newvirtual() virtual = virtual { + sec = 0, + nsec = 0, + loc = chrono::UTC, + zone = null, + date = void, + time = 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, + + vloc = void, + locname = void, + zoff = void, + zabbr = void, + halfhour = void, + ampm = void, +}; + +// Realizes a valid [[datetime]] from a [[virtual]], or fails appropriately. +// Four values require determination. Each has various determination strategies, +// each of which use a certain set of non-void fields from the given virtual. +// The following determination strategies will be attempted in order. +// +// Field sets for determining the date: +// +// 1. date +// 2. year, month, day +// 3. year, yearday +// 4. year, week, weekday +// 5. isoweekyear, isoweek, weekday +// +// Field sets for determining the time: +// +// 1. time +// 2. hour, minute, second, nanosecond +// +// Field sets for determining the zone offset: +// +// 1. zoff +// +// Field sets for determining the [[time::chrono::locality]]: +// +// 1. vloc +// 2. locname +// This is compared to each provided locality's 'name' field, +// or "UTC" if none are provided. The first match is used. +// 3. (none) +// Defaults to [[time::chrono::UTC]]. +// +// If for any of these values no determination strategy could be attempted, +// [[insufficient]] is returned. If the resultant datetime is invalid, +// [[invalid]] is returned. +export fn realize( + v: virtual, + locs: time::chrono::locality... +) (datetime | insufficient | invalid) = { + // determine .date + if (v.date is i64) { + void; + } else if ( + v.year is int && + v.month is int && + v.day is int + ) { + v.date = calc_date__ymd( + v.year as int, + v.month as int, + v.day as int, + )?; + } else if ( + v.year is int && + v.yearday is int + ) { + v.date = calc_date__yd( + v.year as int, + v.yearday as int, + )?; + } else if ( + v.year is int && + v.week is int && + v.weekday is int + ) { + v.date = calc_date__ywd( + v.year as int, + v.week as int, + v.weekday as int, + )?; + } else if (false) { + // TODO: calendar.ha: calc_date__isoywd() + void; + } else { + // cannot deduce date + return insufficient; + }; + + // determine .time + if (v.time is time::duration) { + void; + } else { + const hour = if (v.hour is int) { + yield v.hour as int; + } else if (v.halfhour is int && v.ampm is bool) { + const hr = v.halfhour as int; + const pm = v.ampm as bool; + yield if (pm) hr * 2 else hr; + } else { + return insufficient; + }; + + if ( + v.minute is int && + v.second is int && + v.nanosecond is int + ) { + v.time = calc_time__hmsn( + hour, + v.minute as int, + v.second as int, + v.nanosecond as int, + )?; + } else { + return insufficient; + }; + }; + + // determine zone offset + if (v.zoff is time::duration) { + void; + } else { + return insufficient; + }; + + // determine .loc (defaults to time::chrono::UTC) + if (v.vloc is chrono::locality) { + v.loc = v.vloc as chrono::locality; + } else if (v.locname is str) { + v.loc = chrono::UTC; + for (let i = 0z; i < len(locs); i += 1) { + const loc = locs[i]; + if (loc.name == v.locname as str) { + v.loc = loc; + break; + }; + }; + }; + + // determine .sec, .nsec + const dt = from_moment(chrono::from_datetime( + v.loc, + v.zoff as time::duration, + v.date as i64, + v.time as time::duration, + )); + + // verify zone offset + const z = chrono::mzone(&dt); + if (z.zoff != v.zoff as time::duration) { + return invalid; + }; + + return dt; +}; diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib @@ -191,10 +191,14 @@ datetime() { chronology.ha \ date.ha \ datetime.ha \ + duration.ha \ format.ha \ parse.ha \ + period.ha \ + reckon.ha \ time.ha \ - timezone.ha + timezone.ha \ + virtual.ha gen_ssa -plinux datetime ascii errors fmt io strconv strings strio \ time time::chrono gen_srcs -pfreebsd datetime \ @@ -202,10 +206,14 @@ datetime() { chronology.ha \ date.ha \ datetime.ha \ + duration.ha \ format.ha \ parse.ha \ + period.ha \ + reckon.ha \ time.ha \ - timezone.ha + timezone.ha \ + virtual.ha gen_ssa -pfreebsd datetime ascii errors fmt io strconv strings strio \ time time::chrono } @@ -1343,6 +1351,7 @@ time() { time_chrono() { gen_srcs -plinux time::chrono \ + arithmetic.ha \ +linux.ha \ chronology.ha \ error.ha \ @@ -1353,6 +1362,7 @@ time_chrono() { gen_ssa -plinux time::chrono \ bufio bytes encoding::utf8 endian errors fmt fs io os strconv strings time path gen_srcs -pfreebsd time::chrono \ + arithmetic.ha \ +freebsd.ha \ chronology.ha \ error.ha \ diff --git a/stdlib.mk b/stdlib.mk @@ -1068,10 +1068,14 @@ stdlib_datetime_linux_srcs = \ $(STDLIB)/datetime/chronology.ha \ $(STDLIB)/datetime/date.ha \ $(STDLIB)/datetime/datetime.ha \ + $(STDLIB)/datetime/duration.ha \ $(STDLIB)/datetime/format.ha \ $(STDLIB)/datetime/parse.ha \ + $(STDLIB)/datetime/period.ha \ + $(STDLIB)/datetime/reckon.ha \ $(STDLIB)/datetime/time.ha \ - $(STDLIB)/datetime/timezone.ha + $(STDLIB)/datetime/timezone.ha \ + $(STDLIB)/datetime/virtual.ha $(HARECACHE)/datetime/datetime-linux.ssa: $(stdlib_datetime_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)) @printf 'HAREC \t$@\n' @@ -1085,10 +1089,14 @@ stdlib_datetime_freebsd_srcs = \ $(STDLIB)/datetime/chronology.ha \ $(STDLIB)/datetime/date.ha \ $(STDLIB)/datetime/datetime.ha \ + $(STDLIB)/datetime/duration.ha \ $(STDLIB)/datetime/format.ha \ $(STDLIB)/datetime/parse.ha \ + $(STDLIB)/datetime/period.ha \ + $(STDLIB)/datetime/reckon.ha \ $(STDLIB)/datetime/time.ha \ - $(STDLIB)/datetime/timezone.ha + $(STDLIB)/datetime/timezone.ha \ + $(STDLIB)/datetime/virtual.ha $(HARECACHE)/datetime/datetime-freebsd.ssa: $(stdlib_datetime_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)) @printf 'HAREC \t$@\n' @@ -1995,6 +2003,7 @@ $(HARECACHE)/time/time-freebsd.ssa: $(stdlib_time_freebsd_srcs) $(stdlib_rt) $(s # time::chrono (+linux) stdlib_time_chrono_linux_srcs = \ + $(STDLIB)/time/chrono/arithmetic.ha \ $(STDLIB)/time/chrono/+linux.ha \ $(STDLIB)/time/chrono/chronology.ha \ $(STDLIB)/time/chrono/error.ha \ @@ -2011,6 +2020,7 @@ $(HARECACHE)/time/chrono/time_chrono-linux.ssa: $(stdlib_time_chrono_linux_srcs) # time::chrono (+freebsd) stdlib_time_chrono_freebsd_srcs = \ + $(STDLIB)/time/chrono/arithmetic.ha \ $(STDLIB)/time/chrono/+freebsd.ha \ $(STDLIB)/time/chrono/chronology.ha \ $(STDLIB)/time/chrono/error.ha \ @@ -3302,10 +3312,14 @@ testlib_datetime_linux_srcs = \ $(STDLIB)/datetime/chronology.ha \ $(STDLIB)/datetime/date.ha \ $(STDLIB)/datetime/datetime.ha \ + $(STDLIB)/datetime/duration.ha \ $(STDLIB)/datetime/format.ha \ $(STDLIB)/datetime/parse.ha \ + $(STDLIB)/datetime/period.ha \ + $(STDLIB)/datetime/reckon.ha \ $(STDLIB)/datetime/time.ha \ - $(STDLIB)/datetime/timezone.ha + $(STDLIB)/datetime/timezone.ha \ + $(STDLIB)/datetime/virtual.ha $(TESTCACHE)/datetime/datetime-linux.ssa: $(testlib_datetime_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)) @printf 'HAREC \t$@\n' @@ -3319,10 +3333,14 @@ testlib_datetime_freebsd_srcs = \ $(STDLIB)/datetime/chronology.ha \ $(STDLIB)/datetime/date.ha \ $(STDLIB)/datetime/datetime.ha \ + $(STDLIB)/datetime/duration.ha \ $(STDLIB)/datetime/format.ha \ $(STDLIB)/datetime/parse.ha \ + $(STDLIB)/datetime/period.ha \ + $(STDLIB)/datetime/reckon.ha \ $(STDLIB)/datetime/time.ha \ - $(STDLIB)/datetime/timezone.ha + $(STDLIB)/datetime/timezone.ha \ + $(STDLIB)/datetime/virtual.ha $(TESTCACHE)/datetime/datetime-freebsd.ssa: $(testlib_datetime_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)) @printf 'HAREC \t$@\n' @@ -4260,6 +4278,7 @@ $(TESTCACHE)/time/time-freebsd.ssa: $(testlib_time_freebsd_srcs) $(testlib_rt) $ # time::chrono (+linux) testlib_time_chrono_linux_srcs = \ + $(STDLIB)/time/chrono/arithmetic.ha \ $(STDLIB)/time/chrono/+linux.ha \ $(STDLIB)/time/chrono/chronology.ha \ $(STDLIB)/time/chrono/error.ha \ @@ -4276,6 +4295,7 @@ $(TESTCACHE)/time/chrono/time_chrono-linux.ssa: $(testlib_time_chrono_linux_srcs # time::chrono (+freebsd) testlib_time_chrono_freebsd_srcs = \ + $(STDLIB)/time/chrono/arithmetic.ha \ $(STDLIB)/time/chrono/+freebsd.ha \ $(STDLIB)/time/chrono/chronology.ha \ $(STDLIB)/time/chrono/error.ha \ diff --git a/time/chrono/README b/time/chrono/README @@ -1,11 +1,22 @@ -The time::chrono submodule provides the basis for chronology in Hare, -namely [[timescale]]s (leap second handling), [[timezone]]s, and the -[[moment]] type, an abstracted datetime for external modules to -interface with. +The time::chrono submodule provides the basis for chronology in Hare, namely +[[timescale]]s (leap second handling), [[timezone]]s, and the [[moment]] type, +an abstracted date/time object for external modules to interface with. -For working with the ISO 8601 Gregorian calendar, see the [[datetime]] -submodule. +For working with the ISO 8601 Gregorian calendar, see the [[datetime]] module. -Hare defines a chronology as a system used to name and order moments in -time. In practice, a chronology is the combination of a calendar (for -handling days) and a wall clock (for handling times throughout a day). +Hare defines a chronology as a system used to name and order moments in time. In +practice, a chronology is the combination of a calendar (for handling days) and +a wall clock (for handling times throughout a day). + +This module implements a small chronology of dates & times. A moment observes +certain chronological values according to its [[locality]]. The "observe" +functions [[date]], [[time]], and [[zone]] obtain these values. + +Higher level modules like [[datetime]] expand upon this with more complex +chronological values (years, hours, etc.). The [[datetime::datetime]] type +embeds [[moment]], and other modules implementing other chronologies may +interoperate with their own extension types. + +[[time::instant]]s can be [[convert]]ed via the [[timescale]] interface. The +[[tai]] timescale acts as the central intermediary timescale. Other timescales +are expected to be able to convert their instants to TAI and back. diff --git a/time/chrono/arithmetic.ha b/time/chrono/arithmetic.ha @@ -0,0 +1,76 @@ +// License: MPL-2.0 +// (c) 2023 Byron Torres <b@torresjrjr.com> +use time; + +// Compares two [[moment]]s. Returns -1 if a precedes b, 0 if a and b are +// simultaneous, or +1 if b precedes a. +// +// The moments are compared as [[time::instant]]s; their observed chronological +// values are ignored. +// +// If the moments' associated [[timescale]]s are different, they will be +// converted to [[tai]] instants first. Any [[discontinuity]] occurence will be +// returned. If a discontinuity against TAI amongst the two timescales exist, +// consider converting such instants manually. +export fn compare(a: *moment, b: *moment) (i8 | discontinuity) = { + const (ia, ib) = convertpair(a, b)?; + return time::compare(ia, ib); +}; + +// Returns true if moments a & b are equivalent; otherwise, returns false. +// +// The moments are compared as [[time::instant]]s; their observed chronological +// values are ignored. +// +// If the moments' associated [[timescale]]s are different, they will be +// converted to [[tai]] instants first. Any [[discontinuity]] occurence will be +// returned. If a discontinuity against TAI amongst the two timescales exist, +// consider converting such instants manually. +export fn eq(a: *moment, b: *moment) (bool | discontinuity) = { + return 0 == compare(a, b)?; +}; + +// Returns the [[time::duration]] between two [[moment]]s, from a to b. +// +// The moments are compared as [[time::instant]]s; their observed chronological +// values are ignored. +// +// If the moments' associated [[timescale]]s are different, they will be +// converted to [[tai]] instants first. Any [[discontinuity]] occurence will be +// returned. If a discontinuity against TAI amongst the two timescales exist, +// consider converting such instants manually. +export fn diff(a: *moment, b: *moment) (time::duration | discontinuity) = { + const (ia, ib) = convertpair(a, b)?; + return time::diff(ia, ib); +}; + +// Adds a [[time::duration]] to a [[moment]] with [[time::add]]. +export fn add(m: *moment, d: time::duration) moment = { + return new(m.loc, time::add(*(m: *time::instant), d)); +}; + +fn convertpair( + a: *moment, + b: *moment, +) ((time::instant, time::instant) | discontinuity) = { + let ia = *(a: *time::instant); + let ib = *(b: *time::instant); + + if (a.loc.timescale != b.loc.timescale) { + match (convert(ia, a.loc.timescale, &tai)) { + case let i: time::instant => + ia = i; + case => + return discontinuity; + }; + + match (convert(ib, b.loc.timescale, &tai)) { + case let i: time::instant => + ib = i; + case => + return discontinuity; + }; + }; + + return (ia, ib); +}; diff --git a/time/chrono/chronology.ha b/time/chrono/chronology.ha @@ -9,16 +9,17 @@ export type invalid = !void; // A moment in time within a [[locality]]. Create one with [[new]]. // // Moments extend the [[time::instant]] type and couples it with a [[timescale]] -// via the .loc field. +// via its [[locality]] field. // -// Moments observe a [[date]], time-of-day, and [[zone]], which are evaluated -// and accessed by the [[getdate]], [[gettime]], and [[getzone]] functions. +// This object should be treated as private and immutable. Directly mutating its +// fields causes undefined behavour when used with module functions. Likewise, +// interrogating the fields' type and value (e.g. using match statements) is +// also improper. // -// The [[time::chrono]] modules implements a small chronology of dates & times. -// Higher level modules like [[datetime]] expand upon this with more complex -// chronological values (years, hours, etc.). The [[datetime::datetime]] type -// embeds this type, and other modules implementing other chronologies may -// interoperate by passing pointers. +// Moments observe a date, time-of-day, and [[zone]], which are evaluated and +// obtained with the [[date]], [[time]], and [[mzone]] "observe" functions. +// These values are derived from the embedded instant and locality information, +// and thus are guaranteed to be valid. export type moment = struct { // The embedded [[time::instant]] of this moment time::instant, @@ -26,102 +27,118 @@ export type moment = struct { // The [[locality]] with which to interpret this moment loc: locality, + // The observed [[zone]] + zone: nullable *zone, + // The observed ordinal day (on Earth or otherwise) // since an abitrary epoch, like the Hare epoch 1970-01-01 - date: (date | void), + date: (void | i64), // The observed time since the start of the day - time: (time::duration | void), - - // The observed [[zone]] - zone: (zone | void), + time: (void | time::duration), }; -// An ordinal day since an epoch. The Hare epoch (zeroth day) 1970 Jan 1st is -// used for terrestrial chronologies. -export type date = i64; - -// Creates a new [[moment]]. +// Creates a new [[moment]]. Uses a given [[time::instant]] with a [[timescale]] +// associated with a given [[locality]]. export fn new(loc: locality, inst: time::instant) moment = { return moment { - loc = loc, sec = inst.sec, nsec = inst.nsec, + loc = loc, + zone = null, date = void, time = void, - zone = void, }; }; // Evalutes, caches, and returns a [[moment]]'s observed [[zone]]. -export fn getzone(m: *moment) zone = { +export fn mzone(m: *moment) zone = { match (m.zone) { - case let z: zone => - return z; - case void => - return lookupzone(m); + case let z: *zone => + return *z; + case null => + const z = _lookupzone(m.loc, *(m: *time::instant)); + m.zone = z; + return *z; }; }; // Evaluates, caches, and returns a [[moment]]'s observed epochal date. -export fn getdate(m: *moment) date = { +// +// For moments with [[locality]]s based on the [[utc]], [[tai]], [[gps]], and +// similar timescales, their epoch date should be interpreted as the Unix epoch +// (1970 Janurary 1st). Other timescales may suggest their own interpretations +// applicable to other chronologies. +export fn date(m: *moment) i64 = { match (m.date) { - case let d: date => + case let d: i64 => return d; case void => - return eval_datetime(m).0; + const (d, t) = calc_datetime( + m.loc, *(m: *time::instant), mzone(m).zoff, + ); + m.time = t; + m.date = d; + return d; }; }; // Evaluates, caches, and returns a [[moment]]'s observed time-of-day as a // [[time::duration]] since the start of a day. -export fn gettime(m: *moment) time::duration = { +export fn time(m: *moment) time::duration = { match (m.time) { case let t: time::duration => return t; case void => - return eval_datetime(m).1; + const (d, t) = calc_datetime( + m.loc, *(m: *time::instant), mzone(m).zoff, + ); + m.time = t; + m.date = d; + return t; }; }; -// Evaluates, caches, and returns a [[moment]]'s observed date & time. -fn eval_datetime(m: *moment) (date, time::duration) = { - const i = time::add(*(m: *time::instant), getzone(m).zoffset); - const day = m.loc.daylength; +// Calculates the observed date and time-of-day of a [[time::instant]] in a +// [[locality]] at a particular zone offset. +fn calc_datetime( + loc: locality, + inst: time::instant, + zoff: time::duration, +) (i64, time::duration) = { + const i = time::add(inst, zoff); + const day = loc.daylength; const daysec = day / time::SECOND; const d = if (i.sec >= 0) i.sec / daysec else (i.sec + 1) / daysec - 1; const t = ((i.sec % daysec + daysec) * time::SECOND + i.nsec) % day; - m.time = t; - m.date = d; return (d, t); }; -// Creates a [[moment]] from a given [[locality]], zone offset, [[date]] and +// Creates a [[moment]] from a given [[locality]], zone offset, date, and // time-of-day. export fn from_datetime( loc: locality, zo: time::duration, - d: date, + d: i64, t: time::duration, ) moment = { const inst = calc_instant(loc.daylength, zo, d, t); return moment { - loc = loc, sec = inst.sec, nsec = inst.nsec, + loc = loc, + zone = null, date = d, time = t, - zone = void }; }; fn calc_instant( day: time::duration, // length of a day zo: time::duration, // zone offset - d: date, // date since epoch + d: i64, // date since epoch t: time::duration, // time since start of day ) time::instant = { - // TODO: make sure this works across transitions const daysec = (day / time::SECOND): i64; const dayrem = day % time::SECOND; let i = time::instant { diff --git a/time/chrono/error.ha b/time/chrono/error.ha @@ -6,7 +6,13 @@ use fs; use io; // All possible errors returned from [[time::chrono]]. -export type error = !(invalid | tzdberror | invalidtzif); +export type error = !( + invalid + | invalidtzif + | tzdberror + | discontinuity + | analytical +); // Converts an [[error]] into a human-friendly string. export fn strerror(err: error) const str = { @@ -30,5 +36,9 @@ export fn strerror(err: error) const str = { case invalidtzif => return "Timezone database error: Invalid TZif data"; }; + case discontinuity => + return "A timescale discontinuity caused a misconversion"; + case analytical => + return "The analyical result of a conversion at a timescale discontinuity"; }; }; diff --git a/time/chrono/leapsec.ha b/time/chrono/leapsec.ha @@ -30,8 +30,9 @@ use strings; // Error initializing the [[utc]] [[timescale]]. type utciniterror = !(fs::error | io::error | encoding::utf8::invalid); -// The number of seconds between the years 1900 and 1970. This number is -// deliberately hypothetical since timekeeping before atomic clocks was not +// The number of seconds between the years 1900 and 1970. +// +// This number is hypothetical since timekeeping before atomic clocks was not // accurate enough to account for small changes in time. export def SECS_1900_1970: i64 = 2208988800; diff --git a/time/chrono/timescale.ha b/time/chrono/timescale.ha @@ -2,16 +2,76 @@ // (c) 2021-2022 Byron Torres <b@torresjrjr.com> use time; -// Represents a scale of time; a time standard. +// Represents a scale of time; a time standard. See [[convert]]. export type timescale = struct { name: str, abbr: str, - to_tai: *ts_converter, - from_tai: *ts_converter, + convto: *tsconverter, + convfrom: *tsconverter, +}; + +export type tsconverter = fn(ts: *timescale, i: time::instant) ([]time::instant | void); + +// A discontinuity between two [[timescale]]s caused a one-to-one +// [[time::instant]] conversion to fail. +export type discontinuity = !void; + +// The analytical result of a [[time::instant]] conversion between two +// [[timescale]]s at a point of [[discontinuity]]. +// +// An empty slice represents a nonexistent conversion result. +// A populated (>1) slice represents an ambiguous conversion result. +export type analytical = ![]time::instant; + +// Converts a [[time::instant]] from one [[timescale]] to the next exhaustively. +// The final conversion result is returned. For each active pair of timescales, +// if neither implements conversion from the first to the second, a two-step +// intermediary TAI conversion will occur. If given zero or one timescales, the +// given instant is returned. +export fn convert(i: time::instant, tscs: *timescale...) (time::instant | analytical) = { + let ts: []time::instant = [i]; + let tmps: []time::instant = []; + + for (let j = 1z; j < len(tscs); j += 1) { + let a = tscs[j - 1]; + let b = tscs[j]; + + for (let k = 0z; k < len(ts); k += 1) { + const t = ts[k]; + + // try .convto + match (a.convto(b, t)) { + case let convs: []time::instant => + append(tmps, convs...); + continue; + case void => void; + }; + + // try .convfrom + match (b.convfrom(a, t)) { + case let convs: []time::instant => + append(tmps, convs...); + continue; + case void => void; + }; + + // default to TAI intermediary + const convs = a.convto(&tai, t) as []time::instant; + for (let l = 0z; l < len(convs); l += 1) { + append(tmps, ( + b.convfrom(&tai, convs[l]) as []time::instant + )...); + }; + }; + + // TODO: sort and deduplicate 'ts' here + ts = tmps; + tmps = []; + }; + + return if (len(ts) == 1) ts[0] else ts; }; -// Converts one [[time::instant]] from one [[timescale]] to another. -export type ts_converter = fn(i: time::instant) (time::instant | time::error); // International Atomic Time // @@ -20,12 +80,27 @@ export type ts_converter = fn(i: time::instant) (time::instant | time::error); export const tai: timescale = timescale { name = "International Atomic Time", abbr = "TAI", - to_tai = &conv_tai_tai, - from_tai = &conv_tai_tai, + convto = &tai_convto, + convfrom = &tai_convfrom, +}; + +fn tai_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &tai => + return [i]; + case => + return void; + }; }; -fn conv_tai_tai(i: time::instant) (time::instant | time::error) = { - return i; + +fn tai_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &tai => + return [i]; + case => + return void; + }; }; @@ -45,51 +120,67 @@ fn conv_tai_tai(i: time::instant) (time::instant | time::error) = { // // During a program's initialization, this timescale initializes by loading its // UTC/TAI leap second data from [[UTC_LEAPSECS_FILE]]; otherwise, fails -// silently. If failed, any attempt to consult UTC leapsec data (like calling -// utc.to_tai(), utc.from_tai()) causes an abort. This includes [[chrono::in]]. +// silently. If failed, any attempt to consult UTC leapsec data (e.g. calling +// [[convert]] on UTC) causes an abort. This includes [[chrono::in]]. export const utc: timescale = timescale { name = "Coordinated Universal Time", abbr = "UTC", - to_tai = &conv_utc_tai, - from_tai = &conv_tai_utc, + convto = &utc_convto, + convfrom = &utc_convfrom, }; -fn conv_tai_utc(a: time::instant) (time::instant | time::error) = { - if (!utc_isinitialized) { - abort("utc timescale uninitialized"); - }; +fn utc_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &utc => + return [i]; + case &tai => + if (!utc_isinitialized) { + abort("utc timescale uninitialized"); + }; - const idx = lookup_leaps(&utc_leapsecs, time::unix(a)); - const ofst = utc_leapsecs[idx].1; + const idx = lookup_leaps(&utc_leapsecs, time::unix(i)); + const ofst = utc_leapsecs[idx].1; - if (time::unix(a) == utc_leapsecs[idx].0) { - void; - }; + if (time::unix(i) == utc_leapsecs[idx].0) { + void; + }; - const b = time::instant { - sec = a.sec - 37, - nsec = a.nsec, + const i = time::instant { + sec = i.sec + 37, + nsec = i.nsec, + }; + + return [i]; + case => + return void; }; - return b; }; -fn conv_utc_tai(a: time::instant) (time::instant | time::error) = { - if (!utc_isinitialized) { - abort("utc timescale uninitialized"); - }; +fn utc_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &utc => + return [i]; + case &tai => + if (!utc_isinitialized) { + abort("utc timescale uninitialized"); + }; - const idx = lookup_leaps(&utc_leapsecs, time::unix(a)); - const ofst = utc_leapsecs[idx].1; + const idx = lookup_leaps(&utc_leapsecs, time::unix(i)); + const ofst = utc_leapsecs[idx].1; - if (time::unix(a) == utc_leapsecs[idx].0) { - void; - }; + if (time::unix(i) == utc_leapsecs[idx].0) { + void; + }; + + const i = time::instant { + sec = i.sec - 37, + nsec = i.nsec, + }; - const b = time::instant { - sec = a.sec + 37, - nsec = a.nsec, + return [i]; + case => + return void; }; - return b; }; fn lookup_leaps(list: *[](i64, i64), t: i64) size = { @@ -124,20 +215,34 @@ fn lookup_leaps(list: *[](i64, i64), t: i64) size = { export const gps: timescale = timescale { name = "Global Positioning System Time", abbr = "GPS", - to_tai = &conv_utc_tai, - from_tai = &conv_tai_utc, + convto = &gps_convto, + convfrom = &gps_convfrom, }; // The constant offset between GPS-Time (Global Positioning System Time) and TAI // (International Atomic Time). Used by [[gps]]. def GPS_OFFSET: time::duration = -19 * time::SECOND; -fn conv_tai_gps(a: time::instant) (time::instant | time::error) = { - return time::add(a, +GPS_OFFSET); +fn gps_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &gps => + return [i]; + case &tai => + return [time::add(i, -GPS_OFFSET)]; + case => + void; + }; }; -fn conv_gps_tai(a: time::instant) (time::instant | time::error) = { - return time::add(a, -GPS_OFFSET); +fn gps_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &gps => + return [i]; + case &tai => + return [time::add(i, +GPS_OFFSET)]; + case => + void; + }; }; @@ -149,22 +254,36 @@ fn conv_gps_tai(a: time::instant) (time::instant | time::error) = { export const tt: timescale = timescale { name = "Terrestrial Time", abbr = "TT", - to_tai = &conv_tt_tai, - from_tai = &conv_tai_tt, + convto = &tt_convto, + convfrom = &tt_convfrom, }; // The constant offset between TT (Terrestrial Time) and TAI (International // Atomic Time). Used by [[tt]]. def TT_OFFSET: time::duration = 32184 * time::MILLISECOND; // 32.184 seconds -fn conv_tai_tt(a: time::instant) (time::instant | time::error) = { - return time::add(a, +TT_OFFSET); +fn tt_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &tt => + return [i]; + case &tai => + return [time::add(i, -TT_OFFSET)]; + case => + void; + }; }; -fn conv_tt_tai(a: time::instant) (time::instant | time::error) = { - return time::add(a, -TT_OFFSET); -}; +fn tt_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &tt => + return [i]; + case &tai => + return [time::add(i, +TT_OFFSET)]; + case => + void; + }; +}; // Arthur David Olson had expressed support for Martian time in his timezone // database project <https://data.iana.org/time-zones/theory.html>: @@ -180,8 +299,8 @@ fn conv_tt_tai(a: time::instant) (time::instant | time::error) = { export const mtc: timescale = timescale { name = "Coordinated Mars Time", abbr = "MTC", - to_tai = &conv_mtc_tai, - from_tai = &conv_tai_mtc, + convto = &mtc_convto, + convfrom = &mtc_convfrom, }; // Factor f, where Martian-time * f == Earth-time. @@ -200,46 +319,62 @@ def DELTA_MARSEPOCH_JANSIX: time::duration = 44796 * 24 * time::HOUR; // Earth and Mars. Earth's midnight occurred first. def DELTA_JANSIX_ADJUSTMENT: time::duration = 82944 * time::MILLISECOND; -fn conv_tai_mtc(a: time::instant) (time::instant | time::error) = { - // Get the "Terrestrial Time". - // '!' since TT and TAI are continuous. - const b = tt.from_tai(a)!; +fn mtc_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &mtc => + return [i]; + case &tai => + // Change epoch from that of the Mars Sol Date + // to the Earth-Mars convergence date 2000 Jan 6th. + let i = time::add(i, -DELTA_MARSEPOCH_JANSIX); - // Change epoch from the Unix epoch 1970 Jan 1st (Terrestrial Time) - // to the Earth-Mars convergence date 2000 Jan 6th midnight. - const b = time::add(b, -DELTA_UNIXEPOCH_JANSIX); + // Slightly adjust epoch for the actual Martian midnight. + // Earth's midnight occurred before Mars'. + i = time::add(i, +DELTA_JANSIX_ADJUSTMENT); - // Scale from Earth-time to Mars-time. - const b = time::mult(b, 1.0 / FACTOR_TERRESTRIAL_MARTIAN); + // Scale from Mars-time to Earth-time. + i = time::mult(i, FACTOR_TERRESTRIAL_MARTIAN); - // Slightly adjust epoch for the actual Martian midnight. - // Earth's midnight occurred before Mars'. - const b = time::add(b, -DELTA_JANSIX_ADJUSTMENT); + // Change epoch to the Unix epoch 1970 Jan 1st (Terrestrial Time). + i = time::add(i, +DELTA_UNIXEPOCH_JANSIX); - // Change epoch to that of the Mars Sol Date. - const b = time::add(b, +DELTA_MARSEPOCH_JANSIX); + // Get the TAI time. + // assertion since TT and TAI are continuous. + const ts = tt.convto(&tai, i) as []time::instant; + + return ts; + case => + void; + }; - return b; }; -fn conv_mtc_tai(a: time::instant) (time::instant | time::error) = { - // Change epoch from that of the Mars Sol Date - // to the Earth-Mars convergence date 2000 Jan 6th. - const b = time::add(a, -DELTA_MARSEPOCH_JANSIX); +fn mtc_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &mtc => + return [i]; + case &tai => + // Get the "Terrestrial Time". + // assertion since TT and TAI are continuous. + let i = (tt.convfrom(&tai, i) as []time::instant)[0]; + + // Change epoch from the Unix epoch 1970 Jan 1st (Terrestrial Time) + // to the Earth-Mars convergence date 2000 Jan 6th midnight. + i = time::add(i, -DELTA_UNIXEPOCH_JANSIX); - // Slightly adjust epoch for the actual Martian midnight. - // Earth's midnight occurred before Mars'. - const b = time::add(b, +DELTA_JANSIX_ADJUSTMENT); + // Scale from Earth-time to Mars-time. + i = time::mult(i, 1.0 / FACTOR_TERRESTRIAL_MARTIAN); - // Scale from Mars-time to Earth-time. - const b = time::mult(b, FACTOR_TERRESTRIAL_MARTIAN); + // Slightly adjust epoch for the actual Martian midnight. + // Earth's midnight occurred before Mars'. + i = time::add(i, -DELTA_JANSIX_ADJUSTMENT); - // Change epoch to the Unix epoch 1970 Jan 1st (Terrestrial Time). - const b = time::add(b, +DELTA_UNIXEPOCH_JANSIX); + // Change epoch to that of the Mars Sol Date. + i = time::add(i, +DELTA_MARSEPOCH_JANSIX); - // Get the TAI time. - // '!' since TT and TAI are continuous. - const b = tt.to_tai(b)!; + return [i]; + case => + void; + }; - return b; }; diff --git a/time/chrono/timezone.ha b/time/chrono/timezone.ha @@ -7,17 +7,17 @@ use path; use strings; use time; -// The locality of a [[moment]]. Contains information about how to present a -// moment's chronological values. +// The locality of a [[moment]]. Contains information about how to calculate a +// moment's observed chronological values. export type locality = *timezone; -// A timezone; a political or general region with a ruleset regarding offsets -// for calculating localized civil time. +// A timezone; a political or otherwise theoretical region with a ruleset +// regarding offsets for calculating localized date/time. export type timezone = struct { // The textual identifier ("Europe/Amsterdam") name: str, - // The base timescale (chrono::utc) + // The base timescale (time::chrono::utc) timescale: *timescale, // The duration of a day in this timezone (24 * time::HOUR) @@ -37,10 +37,10 @@ export type timezone = struct { posix_extend: str, }; -// A [[timezone]] state, with an offset for calculating localized civil time. +// A [[timezone]] state, with an offset for calculating localized date/time. export type zone = struct { // The offset from the normal timezone (2 * time::HOUR) - zoffset: time::duration, + zoff: time::duration, // The full descriptive name ("Central European Summer Time") name: str, @@ -70,59 +70,69 @@ type tzname = struct { dst_endtime: str, }; +// Frees a [[timezone]]. A [[locality]] argument can be passed. +export fn timezone_free(tz: *timezone) void = { + free(tz.name); + for (let i = 0z; i < len(tz.zones); i += 1) { + zone_finish(&tz.zones[i]); + }; + free(tz.zones); + free(tz.transitions); + free(tz.posix_extend); + free(tz); +}; + +// Frees resources associated with a [[zone]]. +export fn zone_finish(z: *zone) void = { + free(z.name); + free(z.abbr); +}; + // Creates an equivalent [[moment]] with a different [[locality]]. // -// If the old and new localities have different timescales, a direct conversion -// between them will be tried, and will abort if unsuccessful. To avoid this, -// consider manually converting moments to instants, and those instants between -// timescales. -export fn in(loc: locality, m: moment) moment = { +// If the moment's associated [[timescale]] and the target locality's timescale +// are different, a conversion from one to the other via the TAI timescale will +// be attempted. Any [[discontinuity]] occurrence will be returned. If a +// discontinuity against TAI amongst the two timescales exist, consider +// converting such instants manually. +export fn in(loc: locality, m: moment) (moment | discontinuity) = { + let i = *(&m: *time::instant); if (m.loc.timescale != loc.timescale) { - const i = *(&m: *time::instant); - const i = match (m.loc.timescale.to_tai(i)) { + match (convert(i, m.loc.timescale, loc.timescale)) { + case analytical => + return discontinuity; case let i: time::instant => - yield i; - case time::error => - abort("time::chrono::in(): direct timescale conversion failed"); + return new(loc, i); }; - const i = match (loc.timescale.from_tai(i)) { - case let i: time::instant => - yield i; - case time::error => - abort("time::chrono::in(): direct timescale conversion failed"); - }; - return new(loc, i); }; - return new(loc, *(&m: *time::instant)); + return new(loc, i); }; -// Finds, sets and returns a [[moment]]'s currently observed zone. -export fn lookupzone(m: *moment) zone = { +// Finds and returns a [[moment]]'s currently observed [[zone]]. +fn _lookupzone(loc: locality, inst: time::instant) *zone = { // TODO: https://todo.sr.ht/~sircmpwn/hare/643 - if (len(m.loc.zones) == 0) { - // TODO: what to do? not ideal to assume UTC - abort("lookupzone(): timezones should have at least one zone"); + if (len(loc.zones) == 0) { + abort("time::chrono: Timezone has no zones"); }; - if (len(m.loc.zones) == 1) { - m.zone = m.loc.zones[0]; - return m.zone as zone; + if (len(loc.zones) == 1) { + return &loc.zones[0]; }; if ( - len(m.loc.transitions) == 0 - || time::compare(*(m: *time::instant), m.loc.transitions[0].when) == -1 + len(loc.transitions) == 0 + || time::compare(inst, loc.transitions[0].when) == -1 ) { // TODO: special case abort("lookupzone(): time is before known transitions"); }; let lo = 0z; - let hi = len(m.loc.transitions); + let hi = len(loc.transitions); for (hi - lo > 1) { const mid = lo + (hi - lo) / 2; - const middle = m.loc.transitions[mid].when; - switch (time::compare(*(m: *time::instant), middle)) { + const middle = loc.transitions[mid].when; + switch (time::compare(inst, middle)) { case -1 => hi = mid; case 0 => @@ -134,17 +144,17 @@ export fn lookupzone(m: *moment) zone = { }; }; - m.zone = m.loc.zones[m.loc.transitions[lo].zoneindex]; + const z = &loc.zones[loc.transitions[lo].zoneindex]; // if we've reached the end of the locality's transitions, try its // posix_extend string // // TODO: Unfinished; complete. - if (lo == len(m.loc.transitions) - 1 && m.loc.posix_extend != "") { + if (lo == len(loc.transitions) - 1 && loc.posix_extend != "") { void; }; - return m.zone as zone; + return z; }; // Creates a [[timezone]] with a single [[zone]]. Useful for fixed offsets. @@ -152,7 +162,7 @@ export fn lookupzone(m: *moment) zone = { // // let hawaii = chrono::fixedzone(&chrono::utc, chrono::EARTH_DAY, // chrono::zone { -// zoffset = -10 * time::HOUR, +// zoff = -10 * time::HOUR, // name = "Hawaiian Reef", // abbr = "HARE", // dst = false, @@ -180,16 +190,16 @@ export fn fixedzone(ts: *timescale, daylen: time::duration, z: zone) timezone = // name of both the timezone and its single zero-offset zone. export const LOCAL: locality = &TZ_LOCAL; -def LOCAL_NAME: str = "Local"; +def TZ_LOCAL_NAME: str = "Local"; let TZ_LOCAL: timezone = timezone { - name = LOCAL_NAME, + name = TZ_LOCAL_NAME, timescale = &utc, daylength = EARTH_DAY, zones = [ zone { - zoffset = 0 * time::SECOND, - name = LOCAL_NAME, + zoff = 0 * time::SECOND, + name = TZ_LOCAL_NAME, abbr = "", dst = false, }, @@ -198,20 +208,13 @@ let TZ_LOCAL: timezone = timezone { posix_extend = "", }; -@fini fn free_tzdata() void = { - free(TZ_LOCAL.transitions); - switch(TZ_LOCAL.name) { - case LOCAL_NAME => void; - case => - free(TZ_LOCAL.zones); - }; -}; - -@init fn set_local_timezone() void = { +@init fn init_tz_local() void = { match (os::getenv("TZ")) { - case let zone: str => - TZ_LOCAL = match (tz(zone)) { - case let tz: timezone => + case let timezone: str => + TZ_LOCAL = match (tz(timezone)) { + case let loc: locality => + const tz = *loc; + timezone_free(loc); yield tz; case => return; @@ -240,7 +243,16 @@ let TZ_LOCAL: timezone = timezone { static let buf: [os::BUFSIZ]u8 = [0...]; const file = bufio::buffered(file, buf, []); - match (parse_tzif(&file, &TZ_LOCAL)) { case => void; }; + load_tzif(&file, &TZ_LOCAL): void; + }; +}; + +@fini fn free_tz_local() void = { + free(TZ_LOCAL.transitions); + switch(TZ_LOCAL.name) { + case TZ_LOCAL_NAME => void; + case => + free(TZ_LOCAL.zones); }; }; @@ -253,7 +265,7 @@ const TZ_UTC: timezone = timezone { daylength = EARTH_DAY, zones = [ zone { - zoffset = 0 * time::SECOND, + zoff = 0 * time::SECOND, name = "Universal Coordinated Time", abbr = "UTC", dst = false, @@ -272,7 +284,7 @@ const TZ_TAI: timezone = timezone { daylength = EARTH_DAY, zones = [ zone { - zoffset = 0 * time::SECOND, + zoff = 0 * time::SECOND, name = "International Atomic Time", abbr = "TAI", dst = false, @@ -291,7 +303,7 @@ const TZ_GPS: timezone = timezone { daylength = EARTH_DAY, zones = [ zone { - zoffset = 0 * time::SECOND, + zoff = 0 * time::SECOND, name = "Global Positioning System", abbr = "GPS", dst = false, @@ -310,7 +322,7 @@ const TZ_TT: timezone = timezone { daylength = EARTH_DAY, zones = [ zone { - zoffset = 0 * time::SECOND, + zoff = 0 * time::SECOND, name = "Terrestrial Time", abbr = "TT", dst = false, @@ -329,7 +341,7 @@ const TZ_MTC: timezone = timezone { daylength = MARS_SOL_MARTIAN, zones = [ zone { - zoffset = 0 * time::SECOND, + zoff = 0 * time::SECOND, name = "Coordinated Mars Time", abbr = "MTC", dst = false, diff --git a/time/chrono/tzdb.ha b/time/chrono/tzdb.ha @@ -18,10 +18,14 @@ export type tzdberror = !(invalidtzif | fs::error | io::error); // Invalid TZif data. export type invalidtzif = !void; -// Finds and loads a [[timezone]] from the system's Timezone database, normally -// located at /usr/share/zoneinfo. All timezones provided default to the [[utc]] -// [[timescale]] and [[EARTH_DAY]] day-length. -export fn tz(name: str) (timezone | tzdberror) = { +// Finds, loads, and allocates a [[timezone]] from the system's Timezone +// database, normally located at /usr/share/zoneinfo, and returns it as a +// [[locality]]. Each call returns a new instance. The caller must free the +// return value. +// +// All localities provided default to the [[utc]] [[timescale]] and +// [[EARTH_DAY]] day-length. +export fn tz(name: str) (locality | tzdberror) = { const filepath = path::init(); path::add(&filepath, ZONEINFO_PREFIX, name)!; const fpath = path::string(&filepath); @@ -30,17 +34,17 @@ export fn tz(name: str) (timezone | tzdberror) = { static let buf: [os::BUFSIZ]u8 = [0...]; const bufstrm = bufio::buffered(file, buf, []); - let tz = timezone { - name = name, + let loc = alloc(timezone { + name = strings::dup(name), timescale = &utc, daylength = EARTH_DAY, ... - }; - match (parse_tzif(&bufstrm, &tz)) { + }); + match (load_tzif(&bufstrm, loc)) { case void => io::close(&bufstrm)?; io::close(file)?; - return tz; + return loc; case invalidtzif => io::close(&bufstrm): void; io::close(file): void; @@ -52,11 +56,11 @@ export fn tz(name: str) (timezone | tzdberror) = { }; }; -// Parses data in the TZif "Time Zone Information Format", and initialises the +// Loads data of the TZif "Time Zone Information Format", and initialises the // fields "zones", "transitions", and "posix_extend" of the given [[timezone]]. // // See: https://datatracker.ietf.org/doc/html/rfc8536 -fn parse_tzif(h: io::handle, tz: *timezone) (void | invalidtzif | io::error) = { +fn load_tzif(h: io::handle, tz: *timezone) (void | invalidtzif | io::error) = { const buf1: [1]u8 = [0...]; const buf4: [4]u8 = [0...]; const buf8: [8]u8 = [0...]; @@ -178,12 +182,12 @@ fn parse_tzif(h: io::handle, tz: *timezone) (void | invalidtzif | io::error) = { }; append(footerdata, buf1...); }; - const posix_extend = match (strings::fromutf8(footerdata)) { + const posix_extend = strings::dup(match (strings::fromutf8(footerdata)) { case let s: str => yield s; case encoding::utf8::invalid => return invalidtzif; - }; + }); // assemble structured data @@ -194,11 +198,11 @@ fn parse_tzif(h: io::handle, tz: *timezone) (void | invalidtzif | io::error) = { const zone = zone { ... }; // offset - const zoffset = endian::begetu32(zonedata[idx..idx + 4]): i32; - if (zoffset == -2147483648) { // -2^31 + const zoff = endian::begetu32(zonedata[idx..idx + 4]): i32; + if (zoff == -2147483648) { // -2^31 return invalidtzif; }; - zone.zoffset = zoffset * time::SECOND; + zone.zoff = zoff * time::SECOND; // daylight saving time indicator zone.dst = switch (zonedata[idx + 4]) { @@ -225,12 +229,12 @@ fn parse_tzif(h: io::handle, tz: *timezone) (void | invalidtzif | io::error) = { if (len(bytes) == 0) { // no NUL encountered return invalidtzif; }; - const abbr = match (strings::fromutf8(bytes)) { + const abbr = strings::dup(match (strings::fromutf8(bytes)) { case let s: str => yield s; case encoding::utf8::invalid => return invalidtzif; - }; + }); zone.abbr = strings::dup(abbr); append(zones, zone); diff --git a/time/types.ha b/time/types.ha @@ -35,12 +35,3 @@ export type instant = struct { // Represents a unique interval of time between two [[instant]]s. export type interval = (instant, instant); - -// All error types which are concerned with the handling of [[instant]]s. -export type error = !(ambiguous | nonexistent); - -// The conversion of an [[instant]] has multiple possible results. -export type ambiguous = ![]instant; - -// The conversion of an [[instant]] has no possible result. -export type nonexistent = !void;