Willow Barraco: 3 rtc: new driver Backport time::date and time::chrono as they are on Hare stdlib fixup! Backport time::date and time::chrono 38 files changed, 5154 insertions(+), 519 deletions(-)
mercury/patches/.build.yml: FAILED in 1m6s [rtc: new driver][0] v3 from [Willow Barraco][1] [0]: https://lists.sr.ht/~sircmpwn/helios-devel/patches/43431 [1]: mailto:contact@willowbarraco.fr ✗ #1037501 FAILED mercury/patches/.build.yml https://builds.sr.ht/~sircmpwn/job/1037501
Rebased and touched up for style. Thanks! To git@git.sr.ht:~sircmpwn/mercury e984b70..4ffa426 master -> master
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~sircmpwn/helios-devel/patches/43431/mbox | git am -3Learn more about email & git
Signed-off-by: Willow Barraco <contact@willowbarraco.fr> --- Makefile | 16 +++ cmd/rtc/main.ha | 318 ++++++++++++++++++++++++++++++++++++++++++ cmd/rtc/manifest.ini | 10 ++ cmd/time/Makefile | 8 ++ cmd/time/main.ha | 34 +++++ dev/clock_gen.ha | 39 ++++++ etc/driver.d/00-rtc | 1 + proto/Makefile | 13 ++ proto/dev/clock.ipc | 9 ++ serv/dev/clock_gen.ha | 69 +++++++++ 10 files changed, 517 insertions(+) create mode 100644 cmd/rtc/main.ha create mode 100644 cmd/rtc/manifest.ini create mode 100644 cmd/time/Makefile create mode 100644 cmd/time/main.ha create mode 100644 dev/clock_gen.ha create mode 120000 etc/driver.d/00-rtc create mode 100644 proto/dev/clock.ipc create mode 100644 serv/dev/clock_gen.ha diff --git a/Makefile b/Makefile index 6abfc67..2a9deb4 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,21 @@ sbin/drv/serial: protos cmd/serial/manifest.o bootstrap.tar: sbin/drv/serial .PHONY: sbin/drv/serial +cmd/rtc/manifest.o: cmd/rtc/manifest.ini + objcopy -S -Ibinary -Oelf64-x86-64 \ + --rename-section .data=.manifest,contents \ + $< $@ +sbin/drv/rtc: protos cmd/rtc/manifest.o + mkdir -p sbin/drv + # XXX: This is a bit messy + LDLINKFLAGS=cmd/rtc/manifest.o \ + HAREPATH=. \ + $(HARE) build -t$(HAREARCH) \ + -X^ -T+$(HAREARCH) \ + -o sbin/drv/rtc cmd/rtc/ +bootstrap.tar: sbin/drv/rtc +.PHONY: sbin/drv/rtc + cmd/pcibus/manifest.o: cmd/pcibus/manifest.ini objcopy -S -Ibinary -Oelf64-x86-64 \ --rename-section .data=.manifest,contents \ @@ -137,6 +152,7 @@ include cmd/sleep/Makefile include cmd/testcons/Makefile include cmd/threads/Makefile include cmd/writefile/Makefile +include cmd/time/Makefile bootstrap.tar: tar -cvf bootstrap.tar bin etc sbin README.md diff --git a/cmd/rtc/main.ha b/cmd/rtc/main.ha new file mode 100644 index 0000000..bc35ce6 --- /dev/null +++ b/cmd/rtc/main.ha @@ -0,0 +1,318 @@ +use helios; +use helios::{pollcap, pollflags}; +use sys; +use serv::dev; +use time; +use time::chrono; +use time::date; + +def IOPORT: helios::cap = 0; + +type mode = enum { + BIN, + BCD, +}; + +type format = enum { + TWELVE, + TWENTYFOUR, +}; + +type rtc = struct { + dev::clock, + initial_date: date::date, + initial_time: time::instant, +}; + +type rtccalendar = struct { + sec: u8, + min: u8, + hour: u8, + day: u8, + month: u8, + year: u8, + century: u8, + mode: mode, + format: format, +}; + +export fn main() void = { + let ep: helios::cap = 0; + const registry = helios::service(sys::DEVREGISTRY_ID); + sys::devregistry_new(registry, dev::CLOCK_ID, &ep); + helios::destroy(registry)!; + + const rtc = rtc { + _iface = &rtc_impl, + _endpoint = ep, + initial_date = read_date(), + initial_time = time::now(time::clock::MONOTONIC), + }; + + dev::ready(); + + for (true) { + dev::clock_dispatch(&rtc); + }; +}; + +fn read_rtc_mode() mode = { + helios::ioport_out8(IOPORT, 0x70, 0x0B)!; + const b = helios::ioport_in8(IOPORT, 0x71)!; + + if (b & 4 != 0) { + return mode::BIN; + }; + + return mode::BCD; +}; + +fn read_rtc_format() format = { + helios::ioport_out8(IOPORT, 0x70, 0x0B)!; + const b = helios::ioport_in8(IOPORT, 0x71)!; + + if (b & 2 != 0) { + return format::TWENTYFOUR; + }; + + return format::TWELVE; +}; + +fn read_rtc_calendar() rtccalendar = { + let calendar = rtccalendar { + mode = read_rtc_mode(), + format = read_rtc_format(), + ... + }; + + const nmi = helios::ioport_in8(IOPORT, 0x70)! & 0x80; + + for (true) { + helios::ioport_out8(IOPORT, 0x70, 0x0A | nmi)!; + if (helios::ioport_in8(IOPORT, 0x71)! & (1u8 << 7) == 0) { + break; + }; + }; + + helios::ioport_out8(IOPORT, 0x70, 0x00 | nmi)!; + calendar.sec = helios::ioport_in8(IOPORT, 0x71)!; + + helios::ioport_out8(IOPORT, 0x70, 0x02 | nmi)!; + calendar.min = helios::ioport_in8(IOPORT, 0x71)!; + + helios::ioport_out8(IOPORT, 0x70, 0x04 | nmi)!; + calendar.hour = helios::ioport_in8(IOPORT, 0x71)!; + + helios::ioport_out8(IOPORT, 0x70, 0x07 | nmi)!; + calendar.day = helios::ioport_in8(IOPORT, 0x71)!; + + helios::ioport_out8(IOPORT, 0x70, 0x08 | nmi)!; + calendar.month = helios::ioport_in8(IOPORT, 0x71)!; + + helios::ioport_out8(IOPORT, 0x70, 0x09 | nmi)!; + calendar.year = helios::ioport_in8(IOPORT, 0x71)!; + + helios::ioport_out8(IOPORT, 0x70, 0x32 | nmi)!; + calendar.century = helios::ioport_in8(IOPORT, 0x71)!; + + return calendar; +}; + +fn eqcalendar(ca: rtccalendar, cb: rtccalendar) bool = { + return ca.sec == cb.sec + && ca.min == cb.min + && ca.hour == cb.hour + && ca.day == cb.day + && ca.month == cb.month + && ca.year == cb.year + && ca.century == cb.century; +}; + +fn bcdtobin(value: u8) u8 = { + return ((value & 0xF0) >> 1) + ((value & 0xF0) >> 3) + (value & 0xF); +}; + +// a better way? +fn bintobcd(value: u8) u8 = { + let new = value: u16; + + for (let i = 0; i < 8; i = i+1) { + if ((new & 0xF000) >> 12 >= 5) { + new = new + 0x3000; + }; + if ((new & 0x0F00) >> 8 >= 5) { + new = new + 0x300; + }; + new = new << 1; + }; + + return (new >> 8): u8; +}; + +// This make sure the rtc is valid, but also convert it to 24h format and binary +// mode +fn read_clean_calendar() rtccalendar = { + let pcal = read_rtc_calendar(); + let cal = read_rtc_calendar(); + + for (!eqcalendar(pcal, cal)) { + pcal = cal; + cal = read_rtc_calendar(); + }; + + // We mask this before converting to binary + const pm: u8 = 0; + if (cal.format == format::TWELVE) { + pm = cal.hour & 0x80; + cal.hour = cal.hour & ~0x80; + }; + + if (cal.mode == mode::BCD) { + cal.sec = bcdtobin(cal.sec); + cal.min = bcdtobin(cal.min); + cal.hour = bcdtobin(cal.hour); + cal.day = bcdtobin(cal.day); + cal.month = bcdtobin(cal.month); + cal.year = bcdtobin(cal.year); + cal.century = bcdtobin(cal.century); + cal.mode = mode::BIN; + }; + + if (cal.format == format::TWELVE) { + if (cal.hour == 12) { + cal.hour = 0; + }; + if (pm != 0) { + cal.hour += 12; + }; + cal.format = format::TWENTYFOUR; + }; + + return cal; +}; + +fn read_date() date::date = { + const cal = read_clean_calendar(); + + return date::new( + chrono::LOCAL, + 0, + cal.century: int * 100 + cal.year: int, + cal.month: int, + cal.day: int, + cal.hour: int, + cal.min: int, + cal.sec: int, + )!; +}; + +fn write_calendar(cal: rtccalendar) void = { + let pm = false; + + const format = read_rtc_format(); + const mode = read_rtc_mode(); + + if (format == format::TWELVE + && cal.format == format::TWENTYFOUR + ) { + if (cal.hour >= 12) { + pm = true; + cal.hour = cal.hour - 12; + } else if (cal.hour == 0) { + cal.hour = 12; + }; + }; + + if (mode == mode::BCD + && cal.mode == mode::BIN + ) { + cal.sec = bintobcd(cal.sec); + cal.min = bintobcd(cal.min); + cal.hour = bintobcd(cal.hour); + cal.day = bintobcd(cal.day); + cal.month = bintobcd(cal.month); + cal.year = bintobcd(cal.year); + cal.century = bintobcd(cal.century); + cal.mode = mode::BCD; + }; + + if (format == format::TWELVE + && cal.format == format::TWENTYFOUR + ) { + if (pm) { + cal.hour = cal.hour & 0x80; + }; + cal.format = format::TWELVE; + }; + + const nmi = helios::ioport_in8(IOPORT, 0x70)! & 0x80; + + helios::ioport_out8(IOPORT, 0x70, 0x00 | nmi)!; + helios::ioport_out8(IOPORT, 0x71, cal.sec)!; + + helios::ioport_out8(IOPORT, 0x70, 0x02 | nmi)!; + helios::ioport_out8(IOPORT, 0x71, cal.min)!; + + helios::ioport_out8(IOPORT, 0x70, 0x04 | nmi)!; + helios::ioport_out8(IOPORT, 0x71, cal.hour)!; + + helios::ioport_out8(IOPORT, 0x70, 0x07 | nmi)!; + helios::ioport_out8(IOPORT, 0x71, cal.day)!; + + helios::ioport_out8(IOPORT, 0x70, 0x08 | nmi)!; + helios::ioport_out8(IOPORT, 0x71, cal.month)!; + + helios::ioport_out8(IOPORT, 0x70, 0x09 | nmi)!; + helios::ioport_out8(IOPORT, 0x71, cal.year)!; + + helios::ioport_out8(IOPORT, 0x70, 0x32 | nmi)!; + helios::ioport_out8(IOPORT, 0x71, cal.century)!; +}; + +fn write_date(dt: date::date) void = { + const cal = rtccalendar { + sec = date::second(&dt): u8, + min = date::minute(&dt): u8, + hour = date::hour(&dt): u8, + day = date::day(&dt): u8, + month = date::month(&dt): u8, + year = (date::year(&dt) % 100): u8, + century = (date::year(&dt) / 100): u8, + mode = mode::BIN, + format = format::TWENTYFOUR, + }; + write_calendar(cal); +}; + +const rtc_impl = dev::clock_iface { + get_time = &rtc_get_time, + set_time = &rtc_set_time, +}; + +fn rtc_get_time(obj: *dev::clock) u64 = { + let rtc = obj: *rtc; + + const delta_time = time::diff( + rtc.initial_time, + time::now(time::clock::MONOTONIC) + ); + + const new_date = date::from_instant( + chrono::LOCAL, + time::add(*(&rtc.initial_date: *time::instant), delta_time) + ); + + return time::unix(*(&new_date: *time::instant)): u64; +}; + +fn rtc_set_time(obj: *dev::clock, time: u64) void = { + let rtc = obj: *rtc; + + const instant = time::from_unix(time: i64); + const dt = date::from_instant(chrono::LOCAL, instant); + + write_date(dt); + + rtc.initial_date = dt; + rtc.initial_time = time::now(time::clock::MONOTONIC); +}; diff --git a/cmd/rtc/manifest.ini b/cmd/rtc/manifest.ini new file mode 100644 index 0000000..1db3c8a --- /dev/null +++ b/cmd/rtc/manifest.ini @@ -0,0 +1,10 @@ +[driver] +name=rtc +desc=RTC driver for x86_64 PCs + +[capabilities] +0:ioport = min=70, max=72 +_:cspace = self + +[services] +devregistry= diff --git a/cmd/time/Makefile b/cmd/time/Makefile new file mode 100644 index 0000000..e263454 --- /dev/null +++ b/cmd/time/Makefile @@ -0,0 +1,8 @@ +bin/time: protos + mkdir -p bin + HAREPATH=. \ + $(HARE) build -t$(HAREARCH) \ + -X^ -T+$(HAREARCH) \ + -o bin/time cmd/time/ +bootstrap.tar: bin/time +.PHONY: bin/time diff --git a/cmd/time/main.ha b/cmd/time/main.ha new file mode 100644 index 0000000..2f57416 --- /dev/null +++ b/cmd/time/main.ha @@ -0,0 +1,34 @@ +use errors; +use fmt; +use helios; +use sys; +use dev; +use strconv; +use os; + +export fn main() void = { + const devmgr = helios::service(sys::DEVMGR_ID); + + let rtc: helios::cap = 0; + match (sys::devmgr_open(devmgr, dev::CLOCK_ID, 0, &rtc)) { + case void => + yield; + case errors::noentry => + fmt::println("No such device")!; + return; + }; + + if (1 == len(os::args)) { + const unix_epoch = dev::clock_get_time(rtc); + fmt::printfln("{}", unix_epoch)!; + } else if (2 == len(os::args)) { + const time = match(strconv::stoi64(os::args[1])) { + case let time: i64 => + yield time: u64; + case => + return; + }; + + dev::clock_set_time(rtc, time); + }; +}; diff --git a/dev/clock_gen.ha b/dev/clock_gen.ha new file mode 100644 index 0000000..7b69aec --- /dev/null +++ b/dev/clock_gen.ha @@ -0,0 +1,39 @@ +// This file was generated by ipcgen; do not modify by hand +use helios; +use rt; + +// ID for the clock IPC interface. +export def CLOCK_ID: u32 = 0x4C943BE4; + +// Labels for operations against clock objects. +export type clock_label = enum u64 { + GET_TIME = CLOCK_ID << 16u64 | 1, + SET_TIME = CLOCK_ID << 16u64 | 2, +}; + +export fn clock_get_time( + ep: helios::cap, +) u64 = { + const (tag, a1) = helios::call(ep, + (clock_label::GET_TIME << 16) | (0 << 8) | 0, + ); + switch (rt::label(tag)) { + case 0 => + return a1: u64; + }; +}; + +export fn clock_set_time( + ep: helios::cap, + time: u64, +) void = { + const (tag, a1) = helios::call(ep, + (clock_label::SET_TIME << 16) | (0 << 8) | 1, + time: u64, + ); + switch (rt::label(tag)) { + case 0 => + return a1: void; + }; +}; + diff --git a/etc/driver.d/00-rtc b/etc/driver.d/00-rtc new file mode 120000 index 0000000..28572e0 --- /dev/null +++ b/etc/driver.d/00-rtc @@ -0,0 +1 @@ +/sbin/drv/rtc \ No newline at end of file diff --git a/proto/Makefile b/proto/Makefile index 1be89c9..ec34c58 100644 --- a/proto/Makefile +++ b/proto/Makefile @@ -3,6 +3,7 @@ CLIENT_PROTOS=\ dev/block_gen.ha \ dev/console_gen.ha \ dev/keyboard_gen.ha \ + dev/clock_gen.ha \ dev/serial_gen.ha \ fs/fs_gen.ha \ io/file_gen.ha \ @@ -16,6 +17,7 @@ SERVER_PROTOS=\ serv/dev/block_gen.ha \ serv/dev/console_gen.ha \ serv/dev/keyboard_gen.ha \ + serv/dev/clock_gen.ha \ serv/dev/serial_gen.ha \ serv/fs/fs_gen.ha \ serv/io/file_gen.ha \ @@ -79,6 +81,17 @@ serv/dev/serial_gen.ha: proto/dev/serial.ipc proto/io/file.ipc @mkdir -p serv/dev/ $(IPCGEN) -Iproto/io/file.ipc server < proto/dev/serial.ipc > $@ +# dev::clock +PROTO_DEV_CLOCK=dev/clock_gen.ha serv/dev/clock_gen.ha + +dev/clock_gen.ha: proto/dev/clock.ipc + @mkdir -p dev/ + $(IPCGEN) client < proto/dev/clock.ipc > $@ + +serv/dev/clock_gen.ha: proto/dev/clock.ipc + @mkdir -p serv/dev/ + $(IPCGEN) server < proto/dev/clock.ipc > $@ + # fs::fs PROTO_FS_FS=fs/fs_gen.ha serv/fs/fs_gen.ha diff --git a/proto/dev/clock.ipc b/proto/dev/clock.ipc new file mode 100644 index 0000000..a79fe3a --- /dev/null +++ b/proto/dev/clock.ipc @@ -0,0 +1,9 @@ +namespace dev; + +interface clock { + # Return time in seconds. + call get_time() u64; + + # Set time in seconds. + call set_time(time: u64) void; +}; diff --git a/serv/dev/clock_gen.ha b/serv/dev/clock_gen.ha new file mode 100644 index 0000000..5bf918d --- /dev/null +++ b/serv/dev/clock_gen.ha @@ -0,0 +1,69 @@ +// This file was generated by ipcgen; do not modify by hand +use errors; +use helios; +use rt; + +// ID for the clock IPC interface. +export def CLOCK_ID: u32 = 0x4C943BE4; + +// Implementation callback for clock::get_time; see [[clock_iface]]. +export type fn_clock_get_time = fn(object: *clock) u64; +// Implementation callback for clock::set_time; see [[clock_iface]]. +export type fn_clock_set_time = fn(object: *clock, time: u64) void; + +// Implementation of a [[clock]] object. +export type clock_iface = struct { + get_time: *fn_clock_get_time, + set_time: *fn_clock_set_time, +}; + +// Labels for operations against clock objects. +export type clock_label = enum u64 { + GET_TIME = CLOCK_ID << 16u64 | 1, + SET_TIME = CLOCK_ID << 16u64 | 2, +}; + +// Instance of an clock object. Users may subtype this object to add +// instance-specific state. +export type clock = struct { + _iface: *clock_iface, + _endpoint: helios::cap, +}; + +// Dispatches a recv operation for an [[clock]] object +export fn clock_dispatch( + object: *clock, +) void = { + rt::ipcbuf.tag = 0 << 8; + const (tag, a1) = helios::recv(object._endpoint); + switch (rt::label(tag)) { + case clock_label::GET_TIME => + const rval = object._iface.get_time( + object, + ); + match (helios::reply((0 << 16) | (0 << 8) | 0, rval: u64)) { + case void => + yield; + case errors::invalid_cslot => + yield; // callee stored the reply + case errors::error => + abort(); // TODO + }; + case clock_label::SET_TIME => + object._iface.set_time( + object, + a1: u64, + ); + match (helios::reply((0 << 16) | (0 << 8) | 0)) { + case void => + yield; + case errors::invalid_cslot => + yield; // callee stored the reply + case errors::error => + abort(); // TODO + }; + case => + abort(); // TODO + }; +}; + -- 2.41.0
Signed-off-by: Willow Barraco <contact@willowbarraco.fr> --- time/chrono/README | 13 + time/chrono/arithmetic.ha | 76 +++++ time/chrono/chronology.ha | 159 ++++++++++ time/chrono/error.ha | 44 +++ time/chrono/leapsec.ha | 89 ++++++ time/chrono/timescale.ha | 390 +++++++++++++++++++++++++ time/chrono/timezone.ha | 350 ++++++++++++++++++++++ time/chrono/tzdb.ha | 318 ++++++++++++++++++++ time/date/README | 20 ++ time/date/date.ha | 285 ++++++++++++++++++ time/date/daydate.ha | 597 ++++++++++++++++++++++++++++++++++++++ time/date/daytime.ha | 32 ++ time/date/error.ha | 18 ++ time/date/format.ha | 296 +++++++++++++++++++ time/date/locality.ha | 13 + time/date/observe.ha | 251 ++++++++++++++++ time/date/parithm.ha | 395 +++++++++++++++++++++++++ time/date/parse.ha | 499 +++++++++++++++++++++++++++++++ time/date/period.ha | 69 +++++ time/date/reckon.ha | 483 ++++++++++++++++++++++++++++++ time/date/tarithm.ha | 11 + time/date/virtual.ha | 225 ++++++++++++++ 22 files changed, 4633 insertions(+) create mode 100644 time/chrono/README create mode 100644 time/chrono/arithmetic.ha create mode 100644 time/chrono/chronology.ha create mode 100644 time/chrono/error.ha create mode 100644 time/chrono/leapsec.ha create mode 100644 time/chrono/timescale.ha create mode 100644 time/chrono/timezone.ha create mode 100644 time/chrono/tzdb.ha create mode 100644 time/date/README create mode 100644 time/date/date.ha create mode 100644 time/date/daydate.ha create mode 100644 time/date/daytime.ha create mode 100644 time/date/error.ha create mode 100644 time/date/format.ha create mode 100644 time/date/locality.ha create mode 100644 time/date/observe.ha create mode 100644 time/date/parithm.ha create mode 100644 time/date/parse.ha create mode 100644 time/date/period.ha create mode 100644 time/date/reckon.ha create mode 100644 time/date/tarithm.ha create mode 100644 time/date/virtual.ha diff --git a/time/chrono/README b/time/chrono/README new file mode 100644 index 0000000..12fcdad --- /dev/null +++ b/time/chrono/README @@ -0,0 +1,13 @@ +The time::chrono module provides timescale utilities, and the foundations for +chronology with the [[moment]] type, an abstract, extendable date/time object. +For the Gregorian chronology, see the [[time::date]] module. + +Hare defines a chronology as a system for naming and ordering moments in time. +In practice, it is the combination of a calendar and wall clock. This module +implements a simple date & time chronology with [[moment]], which observes +certain chronological values according to its [[locality]]. The observer +functions [[daydate]], [[daytime]], and [[mzone]] obtain these values. +Use [[in]] to localize a moment to another [[locality]]; consult [[tz]]. + +The [[timescale]] interface facilitates leap-second aware [[convert]]ion of +[[time::instant]]s. The [[tai]] timescale is the default intermediary timescale. diff --git a/time/chrono/arithmetic.ha b/time/chrono/arithmetic.ha new file mode 100644 index 0000000..47ca1ab --- /dev/null +++ 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, x: time::duration) moment = { + return new(m.loc, time::add(*(m: *time::instant), x)); +}; + +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 new file mode 100644 index 0000000..e86764a --- /dev/null +++ b/time/chrono/chronology.ha @@ -0,0 +1,159 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use time; + +// Invalid [[moment]]. +export type invalid = !void; + +// A moment in time within a [[locality]]. Create one with [[new]]. This type +// extends the [[time::instant]] type and couples it with a [[timescale]] via +// its [[locality]] field. +// +// 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. +// +// Moments observe a daydate, time-of-day, and [[zone]], which are evaluated, +// cached and obtained with the observer functions [[daydate]], [[daytime]], and +// [[mzone]]. 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]]. + time::instant, + + // The [[locality]] with which to interpret this moment. + loc: locality, + + // The observed [[zone]]. + zone: nullable *zone, + + // The observed daydate (scalar day number) + // since an abitrary epoch (e.g. the Unix epoch 1970-01-01). + daydate: (void | i64), + + // The observed time-of-day (amount of daytime progressed in a day). + daytime: (void | time::duration), +}; + +// Creates a new [[moment]]. Uses a given [[time::instant]] with a [[timescale]] +// associated with a given [[locality]]. +export fn new(loc: locality, i: time::instant) moment = { + return moment { + sec = i.sec, + nsec = i.nsec, + loc = loc, + zone = null, + daydate = void, + daytime = void, + }; +}; + +// Observes a [[moment]]'s observed [[zone]]. +export fn mzone(m: *moment) zone = { + match (m.zone) { + case let z: *zone => + return *z; + case null => + const z = _lookupzone(m.loc, *(m: *time::instant)); + m.zone = z; + return *z; + }; +}; + +// Observes a [[moment]]'s observed daydate (day number since epoch). +// +// 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 daydate(m: *moment) i64 = { + match (m.daydate) { + case let dd: i64 => + return dd; + case void => + const (dd, dt) = calc_datetime( + m.loc, *(m: *time::instant), mzone(m).zoff, + ); + m.daytime = dt; + m.daydate = dd; + return dd; + }; +}; + +// Observes a [[moment]]'s observed time-of-day (amount of daytime progressed in +// a day) as a [[time::duration]]. +export fn daytime(m: *moment) time::duration = { + match (m.daytime) { + case let dt: time::duration => + return dt; + case void => + const (dd, dt) = calc_datetime( + m.loc, *(m: *time::instant), mzone(m).zoff, + ); + m.daytime = dt; + m.daydate = dd; + return dt; + }; +}; + +// Calculates the observed daydate 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 dd = if (i.sec >= 0) i.sec / daysec else (i.sec + 1) / daysec - 1; + const dt = ((i.sec % daysec + daysec) * time::SECOND + i.nsec) % day; + return (dd, dt); +}; + +// Creates a [[moment]] from a given [[locality]], zone offset, daydate, and +// time-of-day. +export fn from_datetime( + loc: locality, + zo: time::duration, + dd: i64, + dt: time::duration, +) moment = { + const inst = calc_instant(loc.daylength, zo, dd, dt); + return moment { + sec = inst.sec, + nsec = inst.nsec, + loc = loc, + zone = null, + daydate = dd, + daytime = dt, + }; +}; + +fn calc_instant( + day: time::duration, // length of a day + zo: time::duration, // zone offset + dd: i64, // date since epoch + dt: time::duration, // time since start of day +) time::instant = { + const daysec = (day / time::SECOND): i64; + const dayrem = day % time::SECOND; + let i = time::instant { + sec = dd * daysec, + nsec = 0, + }; + i = time::add(i, dd * dayrem); + i = time::add(i, dt); + i = time::add(i, -zo); + return i; +}; + +// The duration of a day on Earth, in terrestrial (SI) seconds. +export def EARTH_DAY: time::duration = 86400 * time::SECOND; + +// The duration of a solar day on Mars, in Martian seconds. +export def MARS_SOL_MARTIAN: time::duration = 86400 * time::SECOND; + +// The duration of a solar day on Mars, in terrestrial (SI) seconds. +export def MARS_SOL_TERRESTRIAL: time::duration = 88775244147000 * time::NANOSECOND; diff --git a/time/chrono/error.ha b/time/chrono/error.ha new file mode 100644 index 0000000..78876f5 --- /dev/null +++ b/time/chrono/error.ha @@ -0,0 +1,44 @@ +// License: MPL-2.0 +// (c) 2022 Byron Torres <b@torresjrjr.com> +use encoding::utf8; +use fmt; +use fs; +use io; + +// All possible errors returned from [[time::chrono]]. +export type error = !( + invalid + | invalidtzif + | tzdberror + | discontinuity + | analytical +); + +// Converts an [[error]] into a human-friendly string. +export fn strerror(err: error) const str = { + match (err) { + case invalid => + return "Invalid moment"; + case invalidtzif => + return "Invalid TZif data"; + case let err: tzdberror => + match (err) { + case let err: fs::error => + return fmt::asprintf( + "Timezone database error: {}", + fs::strerror(err), + ); + case let err: io::error => + return fmt::asprintf( + "Timezone database error: {}", + io::strerror(err), + ); + 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 new file mode 100644 index 0000000..40102cc --- /dev/null +++ b/time/chrono/leapsec.ha @@ -0,0 +1,89 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use bufio; +use encoding::utf8; +use fs; +use io; +use os; +use strconv; +use strings; + +// Hare uses raw leap second information when dealing with the UTC and TAI +// timescales. This information is source from a standard file installed at +// /usr/share/zoneinfo/leap-seconds.list, which itself is fetched from and +// periodically maintained at various observatories. +// +// https://data.iana.org/time-zones/code/leap-seconds.list +// https://www.ietf.org/timezones/data/leap-seconds.list +// ftp://ftp.nist.gov/pub/time/leap-seconds.list +// ftp://ftp.boulder.nist.gov/pub/time/leap-seconds.list +// +// This is in contrast to previous systems which rely on TZif files, which are +// installed typically at /usr/share/zoneinfo, as part of the "Olson" IANA +// Timezone database. These files couple timezone and leap second data. +// +// Depending on a system's installation, leap second information may be +// deliberately left out of the TZif files, or duplicated throughout. This +// design also inhibits our ambitions for dealing with multiple, dynamic +// timescales. Therefore, we have decided to take an alternative approach. + +// 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 hypothetical since timekeeping before atomic clocks was not +// accurate enough to account for small changes in time. +export def SECS_1900_1970: i64 = 2208988800; + +// The filepath of the system's "leap-seconds.list" file, which contains UTC/TAI +// leap second data. +export def UTC_LEAPSECS_FILE: str = "/usr/share/zoneinfo/leap-seconds.list"; + +// UTC/TAI leap second data; UTC timestamps and their offsets from TAI. +// Sourced from [[UTC_LEAPSECS_FILE]]. +let utc_leapsecs: [](i64, i64) = []; + +let utc_isinitialized: bool = false; + +@fini fn free_utc() void = { + free(utc_leapsecs); +}; + +fn init_utc_leapsecs() (void | utciniterror) = { + const file = os::open(UTC_LEAPSECS_FILE)?; + defer io::close(file)!; + parse_utc_leapsecs(file, &utc_leapsecs)?; +}; + +// Parse UTC/TAI leap second data from [[UTC_LEAPSECS_FILE]]. +// See file for format details. +fn parse_utc_leapsecs( + h: io::handle, + leapsecs: *[](i64, i64), +) (void | encoding::utf8::invalid | io::error) = { + for (true) { + const line = match (bufio::scanline(h)) { + case let err: io::error => + return err; + case io::EOF => + return; + case let line: []u8 => + yield strings::fromutf8(line)?; + }; + defer free(line); + if (strings::hasprefix(line, '#')) { + continue; + }; + const pair = strings::splitn(line, "\t", 3); + defer free(pair); + if (len(pair) < 2) { + continue; + }; + const a = strconv::stoi64(pair[0])!; + const b = strconv::stoi64(pair[1])!; + const a = a - SECS_1900_1970; + const pair = (a: i64, b: i64); + append(utc_leapsecs, pair); + }; +}; diff --git a/time/chrono/timescale.ha b/time/chrono/timescale.ha new file mode 100644 index 0000000..cfa76e7 --- /dev/null +++ b/time/chrono/timescale.ha @@ -0,0 +1,390 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use time; + +// Represents a scale of time; a time standard. See [[convert]]. +export type timescale = struct { + name: str, + abbr: str, + 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; +}; + + +// International Atomic Time +// +// The realisation of proper time on Earth's geoid. +// Continuous (no leap seconds). +export const tai: timescale = timescale { + name = "International Atomic Time", + abbr = "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 tai_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &tai => + return [i]; + case => + return void; + }; +}; + + +// TODO: Write proper conversion functions for all timescales. +// +// Ticket: https://todo.sr.ht/~sircmpwn/hare/642 +// +// For UTC, conversion functions are to return two or no instants, depending on +// any leap second events, and use a proper leap second table. See leapsec.ha. + + +// Coordinated Universal Time +// +// Used as the basis of civil timekeeping. +// Based on TAI; time-dependent offset. +// Discontinuous (has leap seconds). +// +// 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 (e.g. calling +// [[convert]] on UTC) causes an abort. This includes [[in]]. +export const utc: timescale = timescale { + name = "Coordinated Universal Time", + abbr = "UTC", + convto = &utc_convto, + convfrom = &utc_convfrom, +}; + +fn utc_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &utc => + return [i]; + case &tai => + if (!utc_isinitialized) { + match (init_utc_leapsecs()) { + case void => + utc_isinitialized = true; + case => + abort("utc timescale uninitialized"); + }; + }; + + const idx = lookup_leaps(&utc_leapsecs, time::unix(i)); + const ofst = utc_leapsecs[idx].1; + + if (time::unix(i) == utc_leapsecs[idx].0) { + void; + }; + + const i = time::instant { + sec = i.sec + 37, + nsec = i.nsec, + }; + + return [i]; + case => + return void; + }; +}; + +fn utc_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { + switch (ts) { + case &utc => + return [i]; + case &tai => + if (!utc_isinitialized) { + match (init_utc_leapsecs()) { + case void => + utc_isinitialized = true; + case => + abort("utc timescale uninitialized"); + }; + }; + + const idx = lookup_leaps(&utc_leapsecs, time::unix(i)); + const ofst = utc_leapsecs[idx].1; + + if (time::unix(i) == utc_leapsecs[idx].0) { + void; + }; + + const i = time::instant { + sec = i.sec - 37, + nsec = i.nsec, + }; + + return [i]; + case => + return void; + }; +}; + +fn lookup_leaps(list: *[](i64, i64), t: i64) size = { + let lo = 0z, hi = len(list); + for (hi - lo > 1) { + const mid = lo + (hi - lo) / 2; + const middle = list[mid].0; + const cmp = time::compare( + time::from_unix(t), + time::from_unix(middle), + ); + switch (cmp) { + case -1 => + hi = mid; + case 0 => + lo = mid; break; + case 1 => + lo = mid; + case => + abort("Unreachable"); + }; + }; + return lo; +}; + + +// Global Positioning System Time +// +// Used for GPS coordination. +// Based on TAI; constant -19 second offset. +// Continuous (no leap seconds). +export const gps: timescale = timescale { + name = "Global Positioning System Time", + abbr = "GPS", + 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 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 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; + }; +}; + + +// Terrestrial Time +// +// Used for astronomical timekeeping. +// Based on TAI; constant +32.184 offset. +// Continuous (no leap seconds). +export const tt: timescale = timescale { + name = "Terrestrial Time", + abbr = "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 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 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>: +// +// > The tz database does not currently support Mars time, but it is documented +// > here in the hopes that support will be added eventually. + +// Coordinated Mars Time +// +// Used for timekeeping on Mars. +// Based on TT; constant factor. +// Continuous (no leap seconds). +export const mtc: timescale = timescale { + name = "Coordinated Mars Time", + abbr = "MTC", + convto = &mtc_convto, + convfrom = &mtc_convfrom, +}; + +// Factor f, where Martian-time * f == Earth-time. +def FACTOR_TERRESTRIAL_MARTIAN: f64 = 1.0274912517; + +// [[time::duration]] in Earth-time between the Unix epoch of 1970 Jan 1st +// midnight, and the Earth-Mars convergence date of 2000 Jan 6th midnight. +def DELTA_UNIXEPOCH_JANSIX: time::duration = 10962 * 24 * time::HOUR; + +// [[time::duration]] in Mars-time between the Mars Sol Date epoch corresponding +// to the Gregorian Earth date 1873 Dec 29th, and the Earth-Mars convergence +// date of 2000 Jan 6. +def DELTA_MARSEPOCH_JANSIX: time::duration = 44796 * 24 * time::HOUR; + +// [[time::duration]] in Mars-time between the midnights of 2000 Jan 6th on +// Earth and Mars. Earth's midnight occurred first. +def DELTA_JANSIX_ADJUSTMENT: time::duration = 82944 * time::MILLISECOND; + +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); + + // Slightly adjust epoch for the actual Martian midnight. + // Earth's midnight occurred before Mars'. + i = time::add(i, DELTA_JANSIX_ADJUSTMENT); + + // Scale from Mars-time to Earth-time. + i = time::mult(i, FACTOR_TERRESTRIAL_MARTIAN); + + // Change epoch to the Unix epoch 1970 Jan 1st (Terrestrial Time). + i = time::add(i, DELTA_UNIXEPOCH_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; + }; + +}; + +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); + + // Scale from Earth-time to Mars-time. + i = time::mult(i, 1.0 / 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 that of the Mars Sol Date. + i = time::add(i, DELTA_MARSEPOCH_JANSIX); + + return [i]; + case => + void; + }; + +}; diff --git a/time/chrono/timezone.ha b/time/chrono/timezone.ha new file mode 100644 index 0000000..7f03034 --- /dev/null +++ b/time/chrono/timezone.ha @@ -0,0 +1,350 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use bufio; +use io; +use os; +use path; +use strings; +use time; + +// 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 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 (time::chrono::utc) + timescale: *timescale, + + // The duration of a day in this timezone (24 * time::HOUR) + daylength: time::duration, + + // The possible temporal zones a locality with this timezone can observe + // (CET, CEST, ...) + zones: []zone, + + // The transitions between this timezone's zones + transitions: []transition, + + // A timezone specifier in the POSIX "expanded" TZ format. + // See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + // + // Used for extending calculations beyond the last known transition. + posix_extend: str, +}; + +// A [[timezone]] state, with an offset for calculating localized date/time. +export type zone = struct { + // The offset from the normal timezone (2 * time::HOUR) + zoff: time::duration, + + // The full descriptive name ("Central European Summer Time") + name: str, + + // The abbreviated name ("CEST") + abbr: str, + + // Indicator of Daylight Saving Time + dst: bool, // true +}; + +// A [[timezone]] transition between two [[zone]]s. +export type transition = struct { + when: time::instant, + zoneindex: int, +}; + +// A destructured dual std/dst POSIX timezone. See tzset(3). +type tzname = struct { + std_name: str, + std_offset: time::duration, + dst_name: str, + dst_offset: time::duration, + dst_start: str, + dst_starttime: str, + dst_end: str, + 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 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) { + match (convert(i, m.loc.timescale, loc.timescale)) { + case analytical => + return discontinuity; + case let i: time::instant => + return new(loc, i); + }; + }; + return new(loc, i); +}; + +// 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(loc.zones) == 0) { + abort("time::chrono: Timezone has no zones"); + }; + + if (len(loc.zones) == 1) { + return &loc.zones[0]; + }; + + if ( + 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(loc.transitions); + for (hi - lo > 1) { + const mid = lo + (hi - lo) / 2; + const middle = loc.transitions[mid].when; + switch (time::compare(inst, middle)) { + case -1 => + hi = mid; + case 0 => + lo = mid; break; + case 1 => + lo = mid; + case => + abort("Unreachable"); + }; + }; + + 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(loc.transitions) - 1 && loc.posix_extend != "") { + void; + }; + + return z; +}; + +// Creates a [[timezone]] with a single [[zone]]. Useful for fixed offsets. +// For example, replicate the civil time Hawaii timezone on Earth: +// +// let hawaii = chrono::fixedzone(&chrono::utc, chrono::EARTH_DAY, +// chrono::zone { +// zoff = -10 * time::HOUR, +// name = "Hawaiian Reef", +// abbr = "HARE", +// dst = false, +// }, +// ); +// +export fn fixedzone(ts: *timescale, daylen: time::duration, z: zone) timezone = { + return timezone { + name = z.name, + timescale = ts, + daylength = daylen, + zones = alloc([z]), + transitions = [], + posix_extend = "", + }; +}; + +// The system's [[locality]]; the system's local [[timezone]]. +// +// This is set during a program's initialisation, where the TZ environment +// variable is tried, otherwise the /etc/localtime file is tried, otherwise a +// default is used. +// +// The default timezone is equivalent to that of [[UTC]], with "Local" being the +// name of both the timezone and its single zero-offset zone. +export const LOCAL: locality = &TZ_LOCAL; + +def TZ_LOCAL_NAME: str = "Local"; + +let TZ_LOCAL: timezone = timezone { + name = TZ_LOCAL_NAME, + timescale = &utc, + daylength = EARTH_DAY, + zones = [ + zone { + zoff = 0 * time::SECOND, + name = TZ_LOCAL_NAME, + abbr = "", + dst = false, + }, + ], + transitions = [], + posix_extend = "", +}; + +@init fn init_tz_local() void = { + match (os::getenv("TZ")) { + case let timezone: str => + match (tz(timezone)) { + case let loc: locality => + TZ_LOCAL = *loc; + case => + return; + }; + case void => + const filepath = match (os::readlink(LOCALTIME_PATH)) { + case let fp: str => + yield fp; + case => + yield LOCALTIME_PATH; + }; + + const file = match (os::open(filepath)) { + case let f: io::file => + yield f; + case => + return; + }; + defer io::close(file)!; + + if (strings::hasprefix(filepath, ZONEINFO_PREFIX)) { + TZ_LOCAL.name = strings::trimprefix( + filepath, ZONEINFO_PREFIX, + ); + }; + + static let buf: [os::BUFSIZ]u8 = [0...]; + const file = bufio::init(file, buf, []); + 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); + }; +}; + +// The UTC (Coordinated Universal Time) "Zulu" [[timezone]] as a [[locality]]. +export const UTC: locality = &TZ_UTC; + +const TZ_UTC: timezone = timezone { + name = "UTC", + timescale = &utc, + daylength = EARTH_DAY, + zones = [ + zone { + zoff = 0 * time::SECOND, + name = "Universal Coordinated Time", + abbr = "UTC", + dst = false, + }, + ], + transitions = [], + posix_extend = "", +}; + +// The TAI (International Atomic Time) "Zulu" [[timezone]] as a [[locality]]. +export const TAI: locality = &TZ_TAI; + +const TZ_TAI: timezone = timezone { + name = "TAI", + timescale = &tai, + daylength = EARTH_DAY, + zones = [ + zone { + zoff = 0 * time::SECOND, + name = "International Atomic Time", + abbr = "TAI", + dst = false, + }, + ], + transitions = [], + posix_extend = "", +}; + +// The GPS (Global Positioning System) "Zulu" [[timezone]] as a [[locality]]. +export const GPS: locality = &TZ_GPS; + +const TZ_GPS: timezone = timezone { + name = "GPS", + timescale = &gps, + daylength = EARTH_DAY, + zones = [ + zone { + zoff = 0 * time::SECOND, + name = "Global Positioning System", + abbr = "GPS", + dst = false, + }, + ], + transitions = [], + posix_extend = "", +}; + +// The TT (Terrestrial Time) "Zulu" [[timezone]] as a [[locality]]. +export const TT: locality = &TZ_TT; + +const TZ_TT: timezone = timezone { + name = "TT", + timescale = &tt, + daylength = EARTH_DAY, + zones = [ + zone { + zoff = 0 * time::SECOND, + name = "Terrestrial Time", + abbr = "TT", + dst = false, + }, + ], + transitions = [], + posix_extend = "", +}; + +// The MTC (Coordinated Mars Time) "Zulu" [[timezone]] as a [[locality]]. +export const MTC: locality = &TZ_MTC; + +const TZ_MTC: timezone = timezone { + name = "MTC", + timescale = &mtc, + daylength = MARS_SOL_MARTIAN, + zones = [ + zone { + zoff = 0 * time::SECOND, + name = "Coordinated Mars Time", + abbr = "MTC", + dst = false, + }, + ], + transitions = [], + posix_extend = "", +}; diff --git a/time/chrono/tzdb.ha b/time/chrono/tzdb.ha new file mode 100644 index 0000000..0ffa23b --- /dev/null +++ b/time/chrono/tzdb.ha @@ -0,0 +1,318 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use bufio; +use bytes; +use encoding::utf8; +use endian; +use errors; +use fs; +use io; +use os; +use path; +use strings; +use time; + +// Error concerning the Timezone database. +export type tzdberror = !(invalidtzif | fs::error | io::error); + +// Invalid TZif data. +export type invalidtzif = !void; + +// 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(ZONEINFO_PREFIX, name)!; + const fpath = path::string(&filepath); + const file = os::open(fpath)?; + + static let buf: [os::BUFSIZ]u8 = [0...]; + const bufstrm = bufio::init(file, buf, []); + + let loc = alloc(timezone { + name = strings::dup(name), + timescale = &utc, + daylength = EARTH_DAY, + ... + }); + match (load_tzif(&bufstrm, loc)) { + case void => + io::close(&bufstrm)?; + io::close(file)?; + return loc; + case invalidtzif => + io::close(&bufstrm): void; + io::close(file): void; + return invalidtzif; + case let err: io::error => + io::close(&bufstrm): void; + io::close(file): void; + return err; + }; +}; + +// 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 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...]; + const buf15: [15]u8 = [0...]; + + // test for magic "TZif" + mustread(h, buf4)?; + if (!bytes::equal(buf4, ['T': u8, 'Z': u8, 'i': u8, 'f': u8])) { + return invalidtzif; + }; + + // read version + mustread(h, buf1)?; + const version = switch (buf1[0]) { + case 0 => + yield 1; + case '2' => + yield 2; + case '3' => + yield 3; + case => + return invalidtzif; + }; + + // skip padding + mustread(h, buf15)?; + + // read counts + mustread(h, buf4)?; let isutcnt = endian::begetu32(buf4); + mustread(h, buf4)?; let isstdcnt = endian::begetu32(buf4); + mustread(h, buf4)?; let leapcnt = endian::begetu32(buf4); + mustread(h, buf4)?; let timecnt = endian::begetu32(buf4); + mustread(h, buf4)?; let typecnt = endian::begetu32(buf4); + mustread(h, buf4)?; let charcnt = endian::begetu32(buf4); + + let is64 = false; + if (version > 1) { + is64 = true; + + // skip to the version 2 data + const skip = ( + // size of version 1 data block + timecnt * 4 + + timecnt + + typecnt * 6 + + charcnt + + leapcnt * 8 + + isstdcnt + + isutcnt + // size of version 2 header + + 20 + ); + for (let i = 0z; i < skip; i += 1) { + mustread(h, buf1)?; + }; + + // read version 2 counts + mustread(h, buf4)?; isutcnt = endian::begetu32(buf4); + mustread(h, buf4)?; isstdcnt = endian::begetu32(buf4); + mustread(h, buf4)?; leapcnt = endian::begetu32(buf4); + mustread(h, buf4)?; timecnt = endian::begetu32(buf4); + mustread(h, buf4)?; typecnt = endian::begetu32(buf4); + mustread(h, buf4)?; charcnt = endian::begetu32(buf4); + }; + + if (typecnt == 0 || charcnt == 0) { + return invalidtzif; + }; + + if (!(isutcnt == 0 || isutcnt == typecnt) + && (isstdcnt == 0 && isstdcnt == typecnt)) { + return invalidtzif; + }; + + const timesz = if (is64) 8 else 4; + + // read data + + const transition_times: []i64 = []; + if (is64) { + readitems8(h, &transition_times, timecnt)?; + } else { + readitems4(h, &transition_times, timecnt)?; + }; + defer free(transition_times); + const zone_indicies: []u8 = []; + readbytes(h, &zone_indicies, timecnt)?; + defer free(zone_indicies); + const zonedata: []u8 = []; + readbytes(h, &zonedata, typecnt * 6)?; + defer free(zonedata); + const abbrdata: []u8 = []; + readbytes(h, &abbrdata, charcnt)?; + defer free(abbrdata); + const leapdata: []u8 = []; + readbytes(h, &leapdata, leapcnt * (timesz: u32 + 4))?; + defer free(leapdata); + const stdwalldata: []u8 = []; + readbytes(h, &stdwalldata, isstdcnt)?; + defer free(stdwalldata); + const normlocaldata: []u8 = []; + readbytes(h, &normlocaldata, isutcnt)?; + defer free(normlocaldata); + // read footer + + let footerdata: []u8 = []; + defer free(footerdata); + mustread(h, buf1)?; + if (buf1[0] != 0x0A) { // '\n' newline + return invalidtzif; + }; + for (true) { + mustread(h, buf1)?; + if (buf1[0] == 0x0A) { // '\n' newline + break; + }; + if (buf1[0] == 0x0) { // cannot contain NUL + return invalidtzif; + }; + append(footerdata, buf1...); + }; + const posix_extend = strings::dup(match (strings::fromutf8(footerdata)) { + case let s: str => + yield s; + case encoding::utf8::invalid => + return invalidtzif; + }); + + // assemble structured data + + // assemble zones + let zones: []zone = []; + for (let i = 0z; i < typecnt; i += 1) { + const idx = i * 6; + const zone = zone { ... }; + + // offset + const zoff = endian::begetu32(zonedata[idx..idx + 4]): i32; + if (zoff == -2147483648) { // -2^31 + return invalidtzif; + }; + zone.zoff = zoff * time::SECOND; + + // daylight saving time indicator + zone.dst = switch (zonedata[idx + 4]) { + case 1u8 => + yield true; + case 0u8 => + yield false; + case => + return invalidtzif; + }; + + // abbreviation + const abbridx = zonedata[idx + 5]; + if (abbridx < 0 || abbridx > (charcnt - 1)) { + return invalidtzif; + }; + let bytes: []u8 = []; + for (let j = abbridx; j < len(abbrdata); j += 1) { + if (abbrdata[j] == 0x0) { + bytes = abbrdata[abbridx..j]; + break; + }; + }; + if (len(bytes) == 0) { // no NUL encountered + return invalidtzif; + }; + const abbr = match (strings::fromutf8(bytes)) { + case let s: str => + yield s; + case encoding::utf8::invalid => + return invalidtzif; + }; + zone.abbr = strings::dup(abbr); + + append(zones, zone); + }; + + // assemble transitions + let transitions: []transition = []; + for (let i = 0z; i < timecnt; i += 1) { + const zoneindex = zone_indicies[i]: int; + if (zoneindex < 0 || zoneindex > (typecnt: int - 1)) { + return invalidtzif; + }; + + const tx = transition { + when = time::instant { + sec = transition_times[i], + ... + }, + zoneindex = zoneindex, + }; + + // stdwalldata and normlocaldata have been omitted, + // until they show their utility. + + append(transitions, tx); + }; + + // commit and return data + tz.zones = zones; + tz.transitions = transitions; + tz.posix_extend = posix_extend; +}; + +fn mustread(h: io::handle, buf: []u8) (void | invalidtzif | io::error) = { + match (io::readall(h, buf)) { + case let err: io::error => + return err; + case io::EOF => + return invalidtzif; + case size => + return; + }; +}; + +fn readbytes( + h: io::handle, + items: *[]u8, + n: size, +) (void | invalidtzif | io::error) = { + const buf: [1]u8 = [0]; + for (let i = 0z; i < n; i += 1) { + mustread(h, buf)?; + const it = buf[0]; + append(items, it); + }; +}; + +fn readitems8( + h: io::handle, + items: *[]i64, + n: size, +) (void | invalidtzif | io::error) = { + const buf: [8]u8 = [0...]; + for (let i = 0z; i < n; i += 1) { + mustread(h, buf)?; + const it = endian::begetu64(buf): i64; + append(items, it); + }; +}; + +fn readitems4( + h: io::handle, + items: *[]i64, + n: size, +) (void | invalidtzif | io::error) = { + const buf: [4]u8 = [0...]; + for (let i = 0z; i < n; i += 1) { + mustread(h, buf)?; + const it = endian::begetu32(buf): i64; + append(items, it); + }; +}; diff --git a/time/date/README b/time/date/README new file mode 100644 index 0000000..317a284 --- /dev/null +++ b/time/date/README @@ -0,0 +1,20 @@ +The time::date module implements the common international Gregorian chronology, +based on the astronomically numbered proleptic Gregorian calendar, as per ISO +8601, and the common 24 hour clock. It provides [[date]], a representation of +civil date/time and a optimized extension of the [[time::chrono::moment]] type. +The [[time::chrono]] module has many useful functions which interoperate with +dates. Any [[time::chrono]] function which accepts *moment also accepts *date. + +Dates are created using [[new]], [[now]], [[nowutc]], or a "from_" function. +Alternatively, the [[virtual]]/[[realize]] interface can handle uncertain or +invalid date/time information, and construct new dates incrementally and safely. +The observer functions ([[year]], [[hour]], etc.) evaluate a date's observed +chronological values, adjusted for its associated [[time::chrono::locality]]. +Use [[in]] to localize a date to another locality; consult [[time::chrono::tz]]. +See [[parse]] and [[format]] for working with date/time strings. + +Date arithmetic operations are categorized into "timescalar" or "chronological". +Timescalar uses [[time::duration]]; see [[add]], [[time::chrono::diff]]. +Chronological uses [[period]]; see [[reckon]], [[pdiff]], [[unitdiff]], +[[truncate]]. Note that calendrical arithmetic is highly irregular due to field +overflows and timezone discontinuities, so think carefully about what you want. diff --git a/time/date/date.ha b/time/date/date.ha new file mode 100644 index 0000000..09d85bd --- /dev/null +++ b/time/date/date.ha @@ -0,0 +1,285 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +// (c) 2022 Drew DeVault <sir@cmpwn.com> +use errors; +use time; +use time::chrono; + +// Invalid [[date]]. +export type invalid = !chrono::invalid; + +// A date/time object; a [[time::chrono::moment]] wrapper optimized for the +// Gregorian chronology, and by extension a [[time::instant]] wrapper. +// +// This object should be treated as private and immutable. Directly mutating its +// fields causes undefined behaviour when used with module functions. Likewise, +// interrogating the fields' type and value (e.g. using match statements) is +// also improper. +// +// A date observes various chronological values, cached in its fields. To +// evaluate and obtain these values, use the various observer functions +// ([[year]], [[hour]], etc.). These values are derived from the embedded moment +// information, and thus are guaranteed to be valid. +// +// See [[virtual]] for an public, mutable, intermediary representation of a +// date, which waives guarantees of validity. +export type date = struct { + chrono::moment, + + era: (void | int), + year: (void | int), + month: (void | int), + day: (void | int), + yearday: (void | int), + isoweekyear: (void | int), + isoweek: (void | int), + week: (void | int), + sundayweek: (void | int), + weekday: (void | int), + + hour: (void | int), + minute: (void | int), + second: (void | int), + nanosecond: (void | int), +}; + +fn init() date = date { + sec = 0, + nsec = 0, + loc = chrono::UTC, + zone = null, + daydate = void, + daytime = void, + + era = void, + year = void, + month = void, + day = void, + yearday = void, + isoweekyear = void, + isoweek = void, + week = void, + sundayweek = void, + weekday = void, + + hour = void, + minute = void, + second = void, + nanosecond = void, +}; + +// Evaluates and populates all of a [[date]]'s fields. +fn all(d: *date) *date = { + _era(d); + _year(d); + _month(d); + _day(d); + _yearday(d); + _isoweekyear(d); + _isoweek(d); + _week(d); + _sundayweek(d); + _weekday(d); + + _hour(d); + _minute(d); + _second(d); + _nanosecond(d); + + return d; +}; + +// Creates a new [[date]]. A maximum of 7 optional field arguments can be given: +// year, month, day-of-month, hour, minute, second, nanosecond. 8 or more causes +// an abort. +// +// // 0000-01-01 00:00:00.000000000 +0000 UTC UTC +// date::new(time::chrono::UTC, 0); +// +// // 2019-12-27 20:07:08.000031415 +0000 UTC UTC +// date::new(time::chrono::UTC, 0, 2019, 12, 27, 20, 07, 08, 31415); +// +// // 2019-12-27 21:00:00.000000000 +0100 CET Europe/Amsterdam +// date::new(time::chrono::tz("Europe/Amsterdam")!, 1 * time::HOUR, +// 2019, 12, 27, 21); +// +// 'zo' is the zone offset from the normal timezone (in most cases, UTC). For +// example, the "Asia/Tokyo" timezone has a single zoff of +9 hours, but the +// "Australia/Sydney" timezone has zoffs +10 hours and +11 hours, as they +// observe Daylight Saving Time. +// +// If specified (non-void), 'zo' must match one of the timezone's observed +// zoffs, or will fail. See [[time::chrono::fixedzone]] for custom timezones. +// +// You may omit the zoff. If the givem timezone has a single zone, [[new]] +// will use that zone's zoff. Otherwise [[new]] will try to infer the zoff +// from the multiple zones. This will fail during certain timezone transitions, +// where certain dates are ambiguous or nonexistent. For example: +// +// - In the Europe/Amsterdam timezone, at 1995 March 26th, +// the local time 02:30 was never observed, +// as the clock jumped forward 1 hour from 02:00 CET to 03:00 CEST. +// +// - In the Europe/Amsterdam timezone, at 1995 September 24th, +// the local time 02:30 was observed twice (00:30 UTC & 01:30 UTC), +// as the clock jumped back 1 hour from 03:00 CEST to 02:00 CET. +export fn new( + loc: chrono::locality, + zo: (time::duration | void), + fields: int... +) (date | invalid) = { + // TODO: + // - revise examples + // - Implement as described. + // - fix calls with `years <= -4715`. + // https://todo.sr.ht/~sircmpwn/hare/565 + let _fields: [_]int = [ + 0, 1, 1, // year month day + 0, 0, 0, 0, // hour min sec nsec + ]; + + assert(len(fields) <= len(_fields), + "time::date::new(): Too many field arguments"); + _fields[..len(fields)] = fields; + + let v = newvirtual(); + + v.vloc = loc; + v.zoff = zo; + v.year = _fields[0]; + v.month = _fields[1]; + v.day = _fields[2]; + v.hour = _fields[3]; + v.minute = _fields[4]; + v.second = _fields[5]; + v.nanosecond = _fields[6]; + + let d = (realize(v, loc) as (date | invalid))?; + + // check if input values are actually observed + if ( + zo as time::duration != chrono::mzone(&d).zoff + || _fields[0] != _year(&d) + || _fields[1] != _month(&d) + || _fields[2] != _day(&d) + || _fields[3] != _hour(&d) + || _fields[4] != _minute(&d) + || _fields[5] != _second(&d) + || _fields[6] != _nanosecond(&d) + ) { + return invalid; + }; + + return d; +}; + +// Returns a [[date]] of the current system time using +// [[time::clock::REALTIME]], in the [[time::chrono::LOCAL]] locality. +export fn now() date = { + return from_instant(chrono::LOCAL, time::now(time::clock::REALTIME)); +}; + +// Returns a [[date]] of the current system time using +// [[time::clock::REALTIME]], in the [[time::chrono::UTC]] locality. +export fn nowutc() date = { + return from_instant(chrono::UTC, time::now(time::clock::REALTIME)); +}; + +// Creates a [[date]] from a [[time::chrono::moment]]. +export fn from_moment(m: chrono::moment) date = { + const d = init(); + d.loc = m.loc; + d.sec = m.sec; + d.nsec = m.nsec; + d.daydate = m.daydate; + d.daytime = m.daytime; + d.zone = m.zone; + return d; +}; + +// Creates a [[date]] from a [[time::instant]] +// in a [[time::chrono::locality]]. +export fn from_instant(loc: chrono::locality, i: time::instant) date = { + return from_moment(chrono::new(loc, i)); +}; + +// Creates a [[date]] from a string, parsed according to a layout format. +// See [[parse]] and [[format]]. At least a complete calendar date has to be +// provided. The if hour, minute, second, nanosecond, or zone offset are not +// provided, they default to 0. +// +// let new = date::from_str( +// date::STAMP_NOZL, +// "2019-12-27 22:07:08.000000000 +0100 CET Europe/Amsterdam", +// locs... +// )!; +// +// The date's [[time::chrono::locality]] will be selected from the provided +// locality arguments. The 'name' field of these localities will be matched +// against the parsed result for the %L specifier. If %L is not specified, or if +// no locality is provided, [[time::chrono::UTC]] is used. +export fn from_str( + layout: str, + s: str, + locs: time::chrono::locality... +) (date | parsefail | insufficient | invalid) = { + const v = newvirtual(); + v.zoff = 0; + v.hour = 0; + v.minute = 0; + v.second = 0; + v.nanosecond = 0; + parse(&v, layout, s)?; + return realize(v, locs...)?; +}; + +@test fn from_str() void = { + let testcases: [_](str, str, []chrono::locality, (date | error)) = [ + (STAMP_NOZL, "2001-02-03 15:16:17.123456789 +0000 UTC UTC", [], + new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17, 123456789)!), + (STAMP, "2001-02-03 15:16:17", [], + new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!), + (RFC3339, "2001-02-03T15:16:17+0000", [], + new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!), + ("%F", "2009-06-30", [], + new(chrono::UTC, 0, 2009, 6, 30)!), + ("%F %L", "2009-06-30 GPS", [chrono::TAI, chrono::GPS], + new(chrono::GPS, 0, 2009, 6, 30)!), + ("%F %T", "2009-06-30 01:02:03", [], + new(chrono::UTC, 0, 2009, 6, 30, 1, 2, 3)!), + ("%FT%T%Z", "2009-06-30T18:30:00Z", [], + new(chrono::UTC, 0, 2009, 6, 30, 18, 30)!), + ("%FT%T.%N%Z", "2009-06-30T18:30:00.987654321Z", [], + new(chrono::UTC, 0, 2009, 6, 30, 18, 30, 0, 987654321)!), + // TODO: for the tests overhaul, when internal test timezones + // are available, check for %L + //("%FT%T%z %L", "2009-06-30T18:30:00+0200 Europe/Amsterdam", [amst], + // new(amst, 2 * time::HOUR, 2009, 6, 30, 18, 30)!), + + ("%Y", "a", [], 'a': parsefail), + ("%X", "2008", [], '2': parsefail), + ]; + + let buf: [64]u8 = [0...]; + for (let i = 0z; i < len(testcases); i += 1) { + const t = testcases[i]; + const expect = t.3; + const actual = from_str(t.0, t.1, t.2...); + + match (expect) { + case let e: date => + assert(actual is date, "wanted 'date', got 'error'"); + assert(chrono::eq(&(actual as date), &e)!, + "incorrect 'date' value"); + case let e: parsefail => + assert(actual is parsefail, + "wanted 'parsefail', got other"); + case insufficient => + assert(actual is insufficient, + "wanted 'insufficient', got other"); + case invalid => + assert(actual is invalid, + "wanted 'invalid', got other"); + }; + }; +}; diff --git a/time/date/daydate.ha b/time/date/daydate.ha new file mode 100644 index 0000000..d1ab736 --- /dev/null +++ b/time/date/daydate.ha @@ -0,0 +1,597 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +// (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net> +use errors; +use time::chrono; + +// Hare internally uses the Unix epoch (1970-01-01) for calendrical logic. Here +// we provide useful constant for working with the astronomically numbered +// proleptic Gregorian calendar, as offsets from the Hare epoch. + +// The Hare epoch of the Julian Day Number. +export def EPOCHAL_JULIAN: i64 = -2440588; + +// The Hare epoch of the Gregorian Common Era. +export def EPOCHAL_GREGORIAN: i64 = -719164; + +// Calculates whether a year is a leap year. +export fn isleapyear(y: int) bool = { + return if (y % 4 != 0) false + else if (y % 100 != 0) true + else if (y % 400 != 0) false + else true; +}; + +// Calculates whether a given year, month, and day-of-month, is a valid date. +fn is_valid_ymd(y: int, m: int, d: int) bool = { + return m >= 1 && m <= 12 && d >= 1 && + d <= calc_month_daycnt(y, m); +}; + +// Calculates whether a given year, and day-of-year, is a valid date. +fn is_valid_yd(y: int, yd: int) bool = { + return yd >= 1 && yd <= calc_year_daycnt(y); +}; + +// Calculates the number of days in the given month of the given year. +fn calc_month_daycnt(y: int, m: int) int = { + const days_per_month: [_]int = [ + 31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + ]; + if (m == 2) { + return if (isleapyear(y)) 29 else 28; + } else { + return days_per_month[m - 1]; + }; +}; + +// Calculates the number of days in a given year. +fn calc_year_daycnt(y: int) int = { + return if (isleapyear(y)) 366 else 365; +}; + +// Calculates the day-of-week of January 1st, given a year. +fn calc_janfirstweekday(y: int) int = { + const y = (y % 400) + 400; // keep year > 0 (using Gregorian cycle) + // Gauss' algorithm + const wd = (5 * ((y - 1) % 4) + + 4 * ((y - 1) % 100) + + 6 * ((y - 1) % 400) + ) % 7; + return wd; +}; + +// Calculates the era, given a year. +fn calc_era(y: int) int = { + return if (y >= 0) { + yield 1; // CE "Common Era" + } else { + yield 0; // BCE "Before Common Era" + }; +}; + +// Calculates the year, month, and day-of-month, given an epochal day. +fn calc_ymd(e: i64) (int, int, int) = { + // Algorithm adapted from: + // https://en.wikipedia.org/wiki/Julian_day#Julian_or_Gregorian_calendar_from_Julian_day_number + // + // Alternate methods of date calculation should be explored. + const J = e - EPOCHAL_JULIAN; + + // TODO: substitute numbers where possible + const b = 274277; + const c = -38; + const j = 1401; + const m = 2; + const n = 12; + const p = 1461; + const r = 4; + const s = 153; + const u = 5; + const v = 3; + const w = 2; + const y = 4716; + + const f = J + j + (((4 * J + b) / 146097) * 3) / 4 + c; + const a = r * f + v; + const g = (a % p) / r; + const h = u * g + w; + + const D = (h % s) / u + 1; + const M = ((h / s + m) % n) + 1; + const Y = (a / p) - y + (n + m - M) / n; + + return (Y: int, M: int, D: int); +}; + +// Calculates the day-of-year, given a year, month, and day-of-month. +fn calc_yearday(y: int, m: int, d: int) int = { + const months_firsts: [_]int = [ + 0, 31, 59, + 90, 120, 151, + 181, 212, 243, + 273, 304, 334, + ]; + + if (m >= 3 && isleapyear(y)) { + return months_firsts[m - 1] + d + 1; + } else { + return months_firsts[m - 1] + d; + }; +}; + +// Calculates the ISO week-numbering year, +// given a year, month, day-of-month, and day-of-week. +fn calc_isoweekyear(y: int, m: int, d: int, wd: int) int = { + if ( + // if the date is within a week whose Thursday + // belongs to the previous Gregorian year + m == 1 && ( + (d == 1 && (wd == 4 || wd == 5 || wd == 6)) + || (d == 2 && (wd == 5 || wd == 6)) + || (d == 3 && wd == 6) + ) + ) { + return y - 1; + } else if ( + // if the date is within a week whose Thursday + // belongs to the next Gregorian year + m == 12 && ( + (d == 29 && wd == 0) + || (d == 30 && (wd == 0 || wd == 1)) + || (d == 31 && (wd == 0 || wd == 1 || wd == 2)) + ) + ) { + return y + 1; + } else { + return y; + }; +}; + +// Calculates the ISO week, +// given a year, week, day-of-week, and day-of-year. +fn calc_isoweek(y: int, w: int) int = { + switch (calc_janfirstweekday(y)) { + case 0 => + return w; + case 1, 2, 3 => + return w + 1; + case 4 => + return if (w != 0) w else 53; + case 5 => + return if (w != 0) w else { + yield if (isleapyear(y - 1)) 53 else 52; + }; + case 6 => + return if (w != 0) w else 52; + case => + abort("Unreachable"); + }; +}; + +// Calculates the week within a Gregorian year [0..53], +// given a day-of-year and day-of-week. +// All days in a year before the year's first Monday belong to week 0. +fn calc_week(yd: int, wd: int) int = { + return (yd + 6 - wd) / 7; +}; + +// Calculates the week within a Gregorian year [0..53], +// given a day-of-year and day-of-week. +// All days in a year before the year's first Sunday belong to week 0. +fn calc_sundayweek(yd: int, wd: int) int = { + return (yd + 6 - ((wd + 1) % 7)) / 7; +}; + +// Calculates the day-of-week, given a epochal day, +// from Monday=0 to Sunday=6. +fn calc_weekday(e: i64) int = { + const wd = ((e + 3) % 7): int; + return (wd + 7) % 7; +}; + +// Calculates the daydate, +// given a year, month, and day-of-month. +fn calc_daydate__ymd(y: int, m: int, d: int) (i64 | invalid) = { + if (!is_valid_ymd(y, m, d)) { + return invalid; + }; + // Algorithm adapted from: + // https://en.wikipedia.org/wiki/Julian_day + // + // TODO: Review, cite, verify, annotate. + const jdn = ( + (1461 * (y + 4800 + (m - 14) / 12)) / 4 + + (367 * (m - 2 - 12 * ((m - 14) / 12))) / 12 + - (3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4 + + d + - 32075 + ); + const e = jdn + EPOCHAL_JULIAN; + return e; +}; + +// Calculates the daydate, +// given a year, week, and day-of-week. +fn calc_daydate__ywd(y: int, w: int, wd: int) (i64 | invalid) = { + const jan1wd = calc_janfirstweekday(y); + const yd = wd - jan1wd + 7 * w; + return calc_daydate__yd(y, yd)?; +}; + +// Calculates the daydate, +// given a year and day-of-year. +fn calc_daydate__yd(y: int, yd: int) (i64 | invalid) = { + if (yd < 1 || yd > calc_year_daycnt(y)) { + return invalid; + }; + return calc_daydate__ymd(y, 1, 1)? + yd - 1; +}; + +@test fn calc_daydate__ymd() void = { + const cases = [ + (( -768, 2, 5), -999999, false), + (( -1, 12, 31), -719529, false), + (( 0, 1, 1), -719528, false), + (( 0, 1, 2), -719527, false), + (( 0, 12, 31), -719163, false), + (( 1, 1, 1), -719162, false), + (( 1, 1, 2), -719161, false), + (( 1965, 3, 23), -1745, false), + (( 1969, 12, 31), -1, false), + (( 1970, 1, 1), 0, false), + (( 1970, 1, 2), 1, false), + (( 1999, 12, 31), 10956, false), + (( 2000, 1, 1), 10957, false), + (( 2000, 1, 2), 10958, false), + (( 2038, 1, 18), 24854, false), + (( 2038, 1, 19), 24855, false), + (( 2038, 1, 20), 24856, false), + (( 2243, 10, 17), 100000, false), + (( 4707, 11, 28), 999999, false), + (( 4707, 11, 29), 1000000, false), + ((29349, 1, 25), 9999999, false), + + (( 1970,-99,-99), 0, true), + (( 1970, -9, -9), 0, true), + (( 1970, -1, -1), 0, true), + (( 1970, 0, 0), 0, true), + (( 1970, 0, 1), 0, true), + (( 1970, 1, 99), 0, true), + (( 1970, 99, 99), 0, true), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const params = cases[i].0; + const expect = cases[i].1; + const should_error = cases[i].2; + const actual = calc_daydate__ymd( + params.0, params.1, params.2, + ); + + if (should_error) { + assert(actual is invalid, "invalid date accepted"); + } else { + assert(actual is i64, "valid date not accepted"); + assert(actual as i64 == expect, "date miscalculation"); + }; + }; +}; + +@test fn calc_daydate__ywd() void = { + const cases = [ + (( -768, 0, 4), -1000034), + (( -768, 5, 4), -999999), + (( -1, 52, 5), -719529), + (( 0, 0, 6), -719528), + (( 0, 0, 7), -719527), + (( 0, 52, 7), -719163), + (( 1, 0, 1), -719162), + (( 1, 0, 2), -719161), + (( 1965, 12, 2), -1745), + (( 1969, 52, 3), -1), + (( 1970, 0, 4), 0), + (( 1970, 0, 5), 1), + (( 1999, 52, 5), 10956), + (( 2000, 0, 6), 10957), + (( 2000, 0, 7), 10958), + (( 2020, 0, 3), 18262), + (( 2022, 9, 1), 19051), + (( 2022, 9, 2), 19052), + (( 2023, 51, 7), 19715), + (( 2024, 8, 3), 19781), + (( 2024, 8, 4), 19782), + (( 2024, 8, 5), 19783), + (( 2024, 49, 4), 20069), + (( 2024, 52, 2), 20088), + (( 2038, 3, 1), 24854), + (( 2038, 3, 2), 24855), + (( 2038, 3, 3), 24856), + (( 2243, 41, 2), 99993), + (( 4707, 47, 4), 999999), + (( 4707, 47, 5), 1000000), + ((29349, 3, 6), 9999999), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const ywd = cases[i].0; + const expected = cases[i].1; + const actual = calc_daydate__ywd(ywd.0, ywd.1, ywd.2)!; + assert(actual == expected, + "incorrect calc_daydate__ywd() result"); + }; +}; + +@test fn calc_daydate__yd() void = { + const cases = [ + ( -768, 36, -999999), + ( -1, 365, -719529), + ( 0, 1, -719528), + ( 0, 2, -719527), + ( 0, 366, -719163), + ( 1, 1, -719162), + ( 1, 2, -719161), + ( 1965, 82, -1745 ), + ( 1969, 365, -1 ), + ( 1970, 1, 0 ), + ( 1970, 2, 1 ), + ( 1999, 365, 10956 ), + ( 2000, 1, 10957 ), + ( 2000, 2, 10958 ), + ( 2038, 18, 24854 ), + ( 2038, 19, 24855 ), + ( 2038, 20, 24856 ), + ( 2243, 290, 100000 ), + ( 4707, 332, 999999 ), + ( 4707, 333, 1000000), + (29349, 25, 9999999), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const y = cases[i].0; + const yd = cases[i].1; + const expected = cases[i].2; + const actual = calc_daydate__yd(y, yd)!; + assert(expected == actual, + "error in date calculation from yd"); + }; + assert(calc_daydate__yd(2020, 0) is invalid, + "calc_daydate__yd() did not reject invalid yearday"); + assert(calc_daydate__yd(2020, 400) is invalid, + "calc_daydate__yd() did not reject invalid yearday"); +}; + +@test fn calc_ymd() void = { + const cases = [ + (-999999, ( -768, 2, 5)), + (-719529, ( -1, 12, 31)), + (-719528, ( 0, 1, 1)), + (-719527, ( 0, 1, 2)), + (-719163, ( 0, 12, 31)), + (-719162, ( 1, 1, 1)), + (-719161, ( 1, 1, 2)), + ( -1745, ( 1965, 3, 23)), + ( -1, ( 1969, 12, 31)), + ( 0, ( 1970, 1, 1)), + ( 1, ( 1970, 1, 2)), + ( 10956, ( 1999, 12, 31)), + ( 10957, ( 2000, 1, 1)), + ( 10958, ( 2000, 1, 2)), + ( 24854, ( 2038, 1, 18)), + ( 24855, ( 2038, 1, 19)), + ( 24856, ( 2038, 1, 20)), + ( 100000, ( 2243, 10, 17)), + ( 999999, ( 4707, 11, 28)), + (1000000, ( 4707, 11, 29)), + (9999999, (29349, 1, 25)), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const paramt = cases[i].0; + const expect = cases[i].1; + const actual = calc_ymd(paramt); + assert(expect.0 == actual.0, "year mismatch"); + assert(expect.1 == actual.1, "month mismatch"); + assert(expect.2 == actual.2, "day mismatch"); + }; +}; + +@test fn calc_yearday() void = { + const cases = [ + (( -768, 2, 5), 36), + (( -1, 12, 31), 365), + (( 0, 1, 1), 1), + (( 0, 1, 2), 2), + (( 0, 12, 31), 366), + (( 1, 1, 1), 1), + (( 1, 1, 2), 2), + (( 1965, 3, 23), 82), + (( 1969, 12, 31), 365), + (( 1970, 1, 1), 1), + (( 1970, 1, 2), 2), + (( 1999, 12, 31), 365), + (( 2000, 1, 1), 1), + (( 2000, 1, 2), 2), + (( 2020, 2, 12), 43), + (( 2038, 1, 18), 18), + (( 2038, 1, 19), 19), + (( 2038, 1, 20), 20), + (( 2243, 10, 17), 290), + (( 4707, 11, 28), 332), + (( 4707, 11, 29), 333), + ((29349, 1, 25), 25), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const params = cases[i].0; + const expect = cases[i].1; + const actual = calc_yearday(params.0, params.1, params.2); + assert(expect == actual, "yearday miscalculation"); + }; +}; + +@test fn calc_week() void = { + const cases = [ + (( 1, 0), 1), + (( 1, 1), 0), + (( 1, 2), 0), + (( 1, 3), 0), + (( 1, 4), 0), + (( 1, 5), 0), + (( 1, 6), 0), + (( 21, 1), 3), + (( 61, 2), 9), + ((193, 4), 27), + ((229, 0), 33), + ((286, 3), 41), + ((341, 6), 48), + ((365, 5), 52), + ((366, 0), 53), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const params = cases[i].0; + const expect = cases[i].1; + const actual = calc_week(params.0, params.1); + assert(expect == actual, "week miscalculation"); + }; +}; + +@test fn calc_sundayweek() void = { + const cases = [ + (( 1, 0), 0), + (( 1, 1), 0), + (( 1, 2), 0), + (( 1, 3), 0), + (( 1, 4), 0), + (( 1, 5), 0), + (( 1, 6), 1), + (( 21, 1), 3), + (( 61, 2), 9), + ((193, 4), 27), + ((229, 0), 33), + ((286, 3), 41), + ((341, 6), 49), + ((365, 5), 52), + ((366, 0), 53), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const params = cases[i].0; + const expect = cases[i].1; + const actual = calc_sundayweek(params.0, params.1); + assert(expect == actual, "week miscalculation"); + }; +}; + +@test fn calc_weekday() void = { + const cases = [ + (-999999, 3), // -0768-02-05 + (-719529, 4), // -0001-12-31 + (-719528, 5), // 0000-01-01 + (-719527, 6), // 0000-01-02 + (-719163, 6), // 0000-12-31 + (-719162, 0), // 0001-01-01 + (-719161, 1), // 0001-01-02 + ( -1745, 1), // 1965-03-23 + ( -1, 2), // 1969-12-31 + ( 0, 3), // 1970-01-01 + ( 1, 4), // 1970-01-02 + ( 10956, 4), // 1999-12-31 + ( 10957, 5), // 2000-01-01 + ( 10958, 6), // 2000-01-02 + ( 24854, 0), // 2038-01-18 + ( 24855, 1), // 2038-01-19 + ( 24856, 2), // 2038-01-20 + ( 100000, 1), // 2243-10-17 + ( 999999, 3), // 4707-11-28 + (1000000, 4), // 4707-11-29 + (9999999, 5), // 29349-01-25 + ]; + for (let i = 0z; i < len(cases); i += 1) { + const paramt = cases[i].0; + const expect = cases[i].1; + const actual = calc_weekday(paramt); + assert(expect == actual, "weekday miscalculation"); + }; +}; + +@test fn calc_janfirstweekday() void = { + const cases = [ + // year weekday + (1969, 2), + (1970, 3), + (1971, 4), + (1972, 5), + (1973, 0), + (1974, 1), + (1975, 2), + (1976, 3), + (1977, 5), + (1978, 6), + (1979, 0), + (1980, 1), + (1981, 3), + (1982, 4), + (1983, 5), + (1984, 6), + (1985, 1), + (1986, 2), + (1987, 3), + (1988, 4), + (1989, 6), + (1990, 0), + (1991, 1), + (1992, 2), + (1993, 4), + (1994, 5), + (1995, 6), + (1996, 0), + (1997, 2), + (1998, 3), + (1999, 4), + (2000, 5), + (2001, 0), + (2002, 1), + (2003, 2), + (2004, 3), + (2005, 5), + (2006, 6), + (2007, 0), + (2008, 1), + (2009, 3), + (2010, 4), + (2011, 5), + (2012, 6), + (2013, 1), + (2014, 2), + (2015, 3), + (2016, 4), + (2017, 6), + (2018, 0), + (2019, 1), + (2020, 2), + (2021, 4), + (2022, 5), + (2023, 6), + (2024, 0), + (2025, 2), + (2026, 3), + (2027, 4), + (2028, 5), + (2029, 0), + (2030, 1), + (2031, 2), + (2032, 3), + (2033, 5), + (2034, 6), + (2035, 0), + (2036, 1), + (2037, 3), + (2038, 4), + (2039, 5), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const paramt = cases[i].0; + const expect = cases[i].1; + const actual = calc_janfirstweekday(paramt); + assert(expect == actual, "calc_janfirstweekday() miscalculation"); + }; +}; diff --git a/time/date/daytime.ha b/time/date/daytime.ha new file mode 100644 index 0000000..b5b2828 --- /dev/null +++ b/time/date/daytime.ha @@ -0,0 +1,32 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use errors; +use time; + +// Calculates the wall clock (hour, minute, second, nanosecond), +// given a time-of-day (amount of daytime progressed in a day). +fn calc_hmsn(t: time::duration) (int, int, int, int) = { + // TODO: Special case for leap seconds, 61st second? + const hour = (t / time::HOUR): int; + const min = ((t / time::MINUTE) % 60): int; + const sec = ((t / time::SECOND) % 60): int; + const nsec = (t % time::SECOND): int; + return (hour, min, sec, nsec); +}; + +// Calculates the time-of-day (amount of daytime progressed in a day), +// given a wall clock (hour, minute, second, nanosecond). +fn calc_daytime__hmsn( + hour: int, + min: int, + sec: int, + nsec: int, +) (time::duration | invalid) = { + const t = ( + (hour * time::HOUR) + + (min * time::MINUTE) + + (sec * time::SECOND) + + (nsec * time::NANOSECOND) + ); + return t; +}; diff --git a/time/date/error.ha b/time/date/error.ha new file mode 100644 index 0000000..78ee7cd --- /dev/null +++ b/time/date/error.ha @@ -0,0 +1,18 @@ +// License: MPL-2.0 +// (c) 2023 Byron Torres <b@torresjrjr.com> + +// All possible errors returned from [[date]]. +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 date information"; + case invalid => + return "Invalid date information"; + case let rn: parsefail => + // TODO: use rune 'rn' here + return "Date parsing error"; + }; +}; diff --git a/time/date/format.ha b/time/date/format.ha new file mode 100644 index 0000000..b5dee19 --- /dev/null +++ b/time/date/format.ha @@ -0,0 +1,296 @@ +// License: MPL-2.0 +// (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 fmt; +use io; +use memio; +use strings; +use time; +use time::chrono; + +// [[format]] layout for the email date format. +export def EMAIL: str = "%a, %d %b %Y %H:%M:%S %z"; + +// [[format]] layout for the email date format, with zone offset and +// zone abbreviation. +export def EMAILZ: str = "%a, %d %b %Y %H:%M:%S %z %Z"; + +// [[format]] layout for the POSIX locale's default date & time representation. +export def POSIX: str = "%a %b %e %H:%M:%S %Y"; + +// [[format]] layout compatible with RFC 3339. +export def RFC3339: str = "%Y-%m-%dT%H:%M:%S%z"; + +// [[format]] layout for a simple timestamp. +export def STAMP: str = "%Y-%m-%d %H:%M:%S"; + +// [[format]] layout for a simple timestamp with nanoseconds. +export def STAMP_NANO: str = "%Y-%m-%d %H:%M:%S.%N"; + +// [[format]] layout for a simple timestamp with nanoseconds and zone +// offset. +export def STAMP_ZOFF: str = "%Y-%m-%d %H:%M:%S.%N %z"; + +// [[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"; + +def WEEKDAYS: [_]str = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +]; + +def WEEKDAYS_SHORT: [_]str = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + +def MONTHS: [_]str = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +def MONTHS_SHORT: [_]str = [ + "Jan", "Feb", "Mar", + "Apr", "May", "Jun", + "Jul", "Aug", "Sep", + "Oct", "Nov", "Dec", +]; + +// TODO: Make format() accept parameters of type (date | period), using the +// "intervals" standard representation provided by ISO 8601? +// +// See https://en.wikipedia.org/wiki/ISO_8601#Time_intervals +// +// Ticket: https://todo.sr.ht/~sircmpwn/hare/650 + +// Formats a [[date]] and writes it into a caller supplied buffer. +// The returned string is borrowed from this buffer. +export fn bsformat( + buf: []u8, + layout: str, + d: *date, +) (str | io::error) = { + let sink = memio::fixed(buf); + format(&sink, layout, d)?; + return memio::string(&sink)!; +}; + +// Formats a [[date]] and writes it into a heap-allocated string. +// The caller must free the return value. +export fn asformat(layout: str, d: *date) (str | io::error) = { + let sink = memio::dynamic(); + format(&sink, layout, d)?; + return memio::string(&sink)!; +}; + +fn fmtout(out: io::handle, r: rune, d: *date) (size | io::error) = { + switch (r) { + case 'a' => + return fmt::fprint(out, WEEKDAYS_SHORT[_weekday(d)]); + case 'A' => + return fmt::fprint(out, WEEKDAYS[_weekday(d)]); + case 'b' => + return fmt::fprint(out, MONTHS_SHORT[_month(d) - 1]); + case 'B' => + return fmt::fprint(out, MONTHS[_month(d) - 1]); + case 'd' => + return fmt::fprintf(out, "{:02}", _day(d)); + case 'e' => + return fmt::fprintf(out, "{: 2}", _day(d)); + case 'F' => + return fmt::fprintf(out, "{:04}-{:02}-{:02}", _year(d), _month(d), _day(d)); + case 'H' => + return fmt::fprintf(out, "{:02}", _hour(d)); + case 'I' => + return fmt::fprintf(out, "{:02}", (_hour(d) + 11) % 12 + 1); + case 'j' => + return fmt::fprintf(out, "{:03}", _yearday(d)); + case 'L' => + return fmt::fprint(out, d.loc.name); + case 'm' => + return fmt::fprintf(out, "{:02}", _month(d)); + case 'M' => + return fmt::fprintf(out, "{:02}", _minute(d)); + case 'N' => + return fmt::fprintf(out, "{:09}", _nanosecond(d)); + case 'p' => + return fmt::fprint(out, if (_hour(d) < 12) "AM" else "PM"); + case 's' => + return fmt::fprintf(out, "{:02}", time::unix(*(d: *time::instant))); + case 'S' => + return fmt::fprintf(out, "{:02}", _second(d)); + case 'T' => + return fmt::fprintf(out, "{:02}:{:02}:{:02}", _hour(d), _minute(d), _second(d)); + case 'u' => + return fmt::fprintf(out, "{}", _weekday(d) + 1); + case 'U' => + return fmt::fprintf(out, "{:02}", _sundayweek(d)); + case 'w' => + return fmt::fprintf(out, "{}", (_weekday(d) + 1) % 7); + case 'W' => + return fmt::fprintf(out, "{:02}", _week(d)); + case 'y' => + return fmt::fprintf(out, "{:02}", _year(d) % 100); + case 'Y' => + return fmt::fprintf(out, "{:04}", _year(d)); + case 'z' => + const (sign, zo) = if (chrono::mzone(d).zoff >= 0) { + yield ('+', calc_hmsn(chrono::mzone(d).zoff)); + } else { + yield ('-', calc_hmsn(-chrono::mzone(d).zoff)); + }; + const (hr, mi) = (zo.0, zo.1); + return fmt::fprintf(out, "{}{:02}{:02}", sign, hr, mi); + case 'Z' => + return fmt::fprint(out, chrono::mzone(d).abbr); + case '%' => + return fmt::fprint(out, "%"); + case => + abort("Invalid format string provided to time::date::format"); + }; +}; + +// Formats a [[date]] according to a layout and writes to an [[io::handle]]. +// +// The layout may contain any of the following format specifiers listed below. +// Implemented are a subset of the POSIX strftime(3) format specifiers, as well +// as some others. Use of unimplemented specifiers or an otherwise invalid +// layout will cause an abort. +// +// %% -- A literal '%' character. +// %a -- The abbreviated name of the day of the week. +// %A -- The full name of the day of the week. +// %b -- The abbreviated name of the month. +// %B -- The full name of the month. +// %d -- The day of the month (decimal, range 01 to 31). +// %e -- The day of the month (decimal, range 1 to 31), left-padded space. +// %F -- The full date, equivalent to %Y-%m-%d +// %H -- The hour of the day as from a 24-hour clock (range 00 to 23). +// %I -- The hour of the day as from a 12-hour clock (range 01 to 12). +// %j -- The ordinal day of the year (range 001 to 366). +// %L -- The locality's name (the timezone's identifier). +// %m -- The month (decimal, range 01 to 12). +// %M -- The minute (decimal, range 00 to 59). +// %N -- The nanosecond of the second (range 000000000 to 999999999). +// %p -- Either "AM" or "PM" according to the current time. +// "AM" includes midnight, and "PM" includes noon. +// %s -- Number of seconds since 1970-01-01 00:00:00, the Unix epoch +// %S -- The second of the minute (range 00 to 60). +// %T -- The full time, equivalent to %H:%M:%S +// %u -- The day of the week (decimal, range 1 to 7). 1 represents Monday. +// %U -- The week number of the current year (range 00 to 53), +// starting with the first Sunday as the first day of week 01. +// %w -- The day of the week (decimal, range 0 to 6). 0 represents Sunday. +// %W -- The week number of the current year (range 00 to 53), +// starting with the first Monday as the first day of week 01. +// %y -- The year without the century digits (range 00 to 99). +// %Y -- The year. +// %z -- The observed zone offset. +// %Z -- The observed zone abbreviation. +// +export fn format( + h: io::handle, + layout: str, + d: *date +) (size | io::error) = { + const iter = strings::iter(layout); + let escaped = false; + let n = 0z; + for (true) { + let r: rune = match (strings::next(&iter)) { + case void => + break; + case let r: rune => + yield r; + }; + + if (escaped) { + escaped = false; + n += fmtout(h, r, d)?; + } else { + if (r == '%') { + escaped = true; + } else { + memio::appendrune(h, r)?; + }; + }; + }; + return n; +}; + +@test fn format() void = { + const d = new(chrono::UTC, 0, 1994, 1, 1, 2, 17, 5, 24)!; + + const cases = [ + // special characters + ("%%", "%"), + // hour + ("%H", "02"), + ("%I", "02"), + // minute + ("%M", "17"), + // second + ("%S", "05"), + // nanosecond + ("%N", "000000024"), + // am/pm + ("%p", "AM"), + // day + ("%d", "01"), + // day + ("%e", " 1"), + // month + ("%m", "01"), + // year + ("%Y", "1994"), + ("%y", "94"), + // month name + ("%b", "Jan"), + ("%B", "January"), + // weekday + ("%u", "6"), + ("%w", "6"), + ("%a", "Sat"), + ("%A", "Saturday"), + // yearday + ("%j", "001"), + // week + ("%W", "00"), + // full date + ("%F", "1994-01-01"), + // full time + ("%T", "02:17:05"), + // Unix timestamp + ("%s", "757390625"), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const layout = cases[i].0; + const expected = cases[i].1; + const actual = asformat(layout, &d)!; + defer free(actual); + if (actual != expected) { + fmt::printfln( + "expected format({}, &d) to be {} but was {}", + layout, expected, actual + )!; + abort(); + }; + }; +}; diff --git a/time/date/locality.ha b/time/date/locality.ha new file mode 100644 index 0000000..eb7ea52 --- /dev/null +++ b/time/date/locality.ha @@ -0,0 +1,13 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +// (c) 2022 Drew DeVault <sir@cmpwn.com> +use time; +use time::chrono; + +// Creates an equivalent [[date]] with a different +// [[time::chrono::locality]]. +// +// The [[time::chrono::discontinuity]] rules from [[time::chrono::in]] apply here. +export fn in(loc: chrono::locality, d: date) (date | chrono::discontinuity) = { + return from_moment(chrono::in(loc, *(&d: *chrono::moment))?); +}; diff --git a/time/date/observe.ha b/time/date/observe.ha new file mode 100644 index 0000000..4da2fb0 --- /dev/null +++ b/time/date/observe.ha @@ -0,0 +1,251 @@ +// License: MPL-2.0 +// (c) 2021-2022 Byron Torres <b@torresjrjr.com> +use errors; +use time; +use time::chrono; + +// These functions are renamed to avoid namespace conflicts, like in the +// parameters of the [[new]] function. + +// Observes a [[date]]'s era. +export fn era(d: *date) int = _era(d); + +// Observes a [[date]]'s year. +export fn year(d: *date) int = _year(d); + +// Observes a [[date]]'s month of the year. +export fn month(d: *date) int = _month(d); + +// Observes a [[date]]'s day of the month. +export fn day(d: *date) int = _day(d); + +// Observes a [[date]]'s day of the week; Monday=0 to Sunday=6. +export fn weekday(d: *date) int = _weekday(d); + +// Observes a [[date]]'s ordinal day of the year. +export fn yearday(d: *date) int = _yearday(d); + +// Observes a [[date]]'s ISO week-numbering year. +export fn isoweekyear(d: *date) int = _isoweekyear(d); + +// Observes a [[date]]'s Gregorian week starting Monday. +export fn week(d: *date) int = _week(d); + +// Observes a [[date]]'s Gregorian week starting Sunday. +export fn sundayweek(d: *date) int = _sundayweek(d); + +// Observes a [[date]]'s ISO week. +export fn isoweek(d: *date) int = _isoweek(d); + +// Observes a [[date]]'s hour of the day. +export fn hour(d: *date) int = _hour(d); + +// Observes a [[date]]'s minute of the hour. +export fn minute(d: *date) int = _minute(d); + +// Observes a [[date]]'s second of the minute. +export fn second(d: *date) int = _second(d); + +// Observes a [[date]]'s nanosecond of the second. +export fn nanosecond(d: *date) int = _nanosecond(d); + +fn _era(d: *date) int = { + match (d.era) { + case void => + d.era = calc_era( + _year(d), + ); + return d.era: int; + case let a: int => + return a; + }; +}; + +fn _year(d: *date) int = { + match (d.year) { + case void => + const ymd = calc_ymd( + chrono::daydate(d), + ); + d.year = ymd.0; + d.month = ymd.1; + d.day = ymd.2; + return d.year: int; + case let y: int => + return y; + }; +}; + +fn _month(d: *date) int = { + match (d.month) { + case void => + const ymd = calc_ymd( + chrono::daydate(d), + ); + d.year = ymd.0; + d.month = ymd.1; + d.day = ymd.2; + return d.month: int; + case let y: int => + return y; + }; +}; + +fn _day(d: *date) int = { + match (d.day) { + case void => + const ymd = calc_ymd( + chrono::daydate(d), + ); + d.year = ymd.0; + d.month = ymd.1; + d.day = ymd.2; + return d.day: int; + case let y: int => + return y; + }; +}; + +fn _weekday(d: *date) int = { + match (d.weekday) { + case void => + d.weekday = calc_weekday( + chrono::daydate(d), + ); + return d.weekday: int; + case let y: int => + return y; + }; +}; + +fn _yearday(d: *date) int = { + match (d.yearday) { + case void => + d.yearday = calc_yearday( + _year(d), + _month(d), + _day(d), + ); + return d.yearday: int; + case let yd: int => + return yd; + }; +}; + +fn _isoweekyear(d: *date) int = { + match (d.isoweekyear) { + case void => + d.isoweekyear = calc_isoweekyear( + _year(d), + _month(d), + _day(d), + _weekday(d), + ); + return d.isoweekyear: int; + case let iwy: int => + return iwy; + }; +}; + +fn _week(d: *date) int = { + match (d.week) { + case void => + d.week = calc_week( + _yearday(d), + _weekday(d), + ); + return d.week: int; + case let w: int => + return w; + }; +}; + +fn _sundayweek(d: *date) int = { + match (d.sundayweek) { + case void => + d.sundayweek = calc_sundayweek( + _yearday(d), + _weekday(d), + ); + return d.sundayweek: int; + case let w: int => + return w; + }; +}; + +fn _isoweek(d: *date) int = { + match (d.isoweek) { + case void => + d.isoweek = calc_isoweek( + _year(d), + _week(d), + ); + return d.isoweek: int; + case let iw: int => + return iw; + }; +}; + +fn _hour(d: *date) int = { + match (d.hour) { + case void => + const hmsn = calc_hmsn( + chrono::daytime(d), + ); + d.hour = hmsn.0; + d.minute = hmsn.1; + d.second = hmsn.2; + d.nanosecond = hmsn.3; + return d.hour: int; + case let h: int => + return h; + }; +}; + +fn _minute(d: *date) int = { + match (d.minute) { + case void => + const hmsn = calc_hmsn( + chrono::daytime(d), + ); + d.hour = hmsn.0; + d.minute = hmsn.1; + d.second = hmsn.2; + d.nanosecond = hmsn.3; + return d.minute: int; + case let m: int => + return m; + }; +}; + +fn _second(d: *date) int = { + match (d.second) { + case void => + const hmsn = calc_hmsn( + chrono::daytime(d), + ); + d.hour = hmsn.0; + d.minute = hmsn.1; + d.second = hmsn.2; + d.nanosecond = hmsn.3; + return d.second: int; + case let s: int => + return s; + }; +}; + +fn _nanosecond(d: *date) int = { + match (d.nanosecond) { + case void => + const hmsn = calc_hmsn( + chrono::daytime(d), + ); + d.hour = hmsn.0; + d.minute = hmsn.1; + d.second = hmsn.2; + d.nanosecond = hmsn.3; + return d.nanosecond: int; + case let n: int => + return n; + }; +}; diff --git a/time/date/parithm.ha b/time/date/parithm.ha new file mode 100644 index 0000000..7a7eef8 --- /dev/null +++ b/time/date/parithm.ha @@ -0,0 +1,395 @@ +// License: MPL-2.0 +// (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 fmt; +use time; +use time::chrono; + +// The nominal units of the Gregorian chronology. Used for chronological +// arithmetic. +export type unit = enum int { + ERA, + YEAR, + MONTH, + WEEK, + DAY, + HOUR, + MINUTE, + SECOND, + NANOSECOND, +}; + +// Calculates the [[period]] between two [[date]]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: date, b: date) period = { + let p = period { ... }; + + if (chrono::compare(&a, &b) == 0) { + return p; + }; + + let reverse = if (chrono::compare(&a, &b) > 0) true else false; + if (reverse) { + let tmp = a; + a = b; + b = tmp; + }; + + p.years = _year(&b) - _year(&a); + + p.months = _month(&b) - _month(&a); + if (p.months < 0) { + p.years -= 1; + p.months += 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; + }; + daycnt = calc_month_daycnt(year, month); + + p.months -= 1; + if (p.months < 0) { + p.years -= 1; + p.months += 12; + }; + p.days += daycnt; + }; + + p.hours = _hour(&b) - _hour(&a); + if (p.hours < 0) { + p.days -= 1; + p.hours += 24; + }; + + p.minutes = _minute(&b) - _minute(&a); + if (p.minutes < 0) { + p.hours -= 1; + p.minutes += 60; + }; + + p.seconds = _second(&b) - _second(&a); + if (p.seconds < 0) { + p.minutes -= 1; + p.seconds += 60; + }; + + p.nanoseconds = _nanosecond(&b) - _nanosecond(&a); + if (p.nanoseconds < 0) { + p.seconds -= 1; + p.nanoseconds += 1000000000; // 10E9 + }; + + return if (reverse) neg(p) else p; +}; + +// Calculates the nominal [[unit]] difference between two [[date]]s. +export fn unitdiff(a: date, b: date, u: unit) i64 = { + switch (u) { + case unit::ERA => + return era(&b) - era(&a); + case unit::YEAR => + return pdiff(a, b).years; + case unit::MONTH => + const d = pdiff(a, b); + return d.years * 12 + d.months; + case unit::WEEK => + return unitdiff(a, b, unit::DAY) / 7; + case unit::DAY => + return chrono::daydate(&b) - chrono::daydate(&a); + case unit::HOUR => + return unitdiff(a, b, unit::DAY) * 24 + pdiff(a, b).hours; + case unit::MINUTE => + return unitdiff(a, b, unit::HOUR) * 60 + pdiff(a, b).minutes; + case unit::SECOND => + return unitdiff(a, b, unit::MINUTE) * 60 + pdiff(a, b).seconds; + case unit::NANOSECOND => + return unitdiff(a, b, unit::SECOND) * 1000000000 + pdiff(a, b).nanoseconds; + }; +}; + +// Truncates the given [[date]] at the provided nominal [[unit]]. +// +// 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(d: date, u: unit) date = { + // 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(d.loc, chrono::mzone(&d).zoff, + 1, 1, 1, + 0, 0, 0, 0, + )!; + case unit::YEAR => + yield new(d.loc, chrono::mzone(&d).zoff, + _year(&d), 1, 1, + 0, 0, 0, 0, + )!; + case unit::MONTH => + yield new(d.loc, chrono::mzone(&d).zoff, + _year(&d), _month(&d), 1, + 0, 0, 0, 0, + )!; + case unit::WEEK => + const dd = chrono::daydate(&d) - _weekday(&d); + const ymd = calc_ymd(dd); + yield new(d.loc, chrono::mzone(&d).zoff, + ymd.0, ymd.1, ymd.2, + 0, 0, 0, 0, + )!; + case unit::DAY => + yield new(d.loc, chrono::mzone(&d).zoff, + _year(&d), _month(&d), _day(&d), + 0, 0, 0, 0, + )!; + case unit::HOUR => + yield new(d.loc, chrono::mzone(&d).zoff, + _year(&d), _month(&d), _day(&d), + _hour(&d), 0, 0, 0, + )!; + case unit::MINUTE => + yield new(d.loc, chrono::mzone(&d).zoff, + _year(&d), _month(&d), _day(&d), + _hour(&d), _minute(&d), 0, 0, + )!; + case unit::SECOND => + yield new(d.loc, chrono::mzone(&d).zoff, + _year(&d), _month(&d), _day(&d), + _hour(&d), _minute(&d), _second(&d), 0, + )!; + case unit::NANOSECOND => + yield d; + }; +}; + +@test fn pdiff() void = { + const cases = [ + ( + new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 2022, 2, 16, 0, 0, 0, 0)!, + period { + years = 1, + months = 1, + days = 1, + ... + }, + ), + ( + new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 2022, 3, 27, 0, 0, 0, 0)!, + period { + years = 1, + months = 2, + days = 12, + ... + }, + ), + ( + new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 2022, 3, 14, 0, 0, 0, 0)!, + period { + years = 1, + months = 1, + days = 27, + ... + }, + ), + ( + new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 2021, 1, 16, 0, 0, 0, 0)!, + period { + days = 1, + ... + }, + ), + ( + new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 2021, 1, 16, 1, 3, 2, 4)!, + period { + days = 1, + hours = 1, + minutes = 3, + seconds = 2, + nanoseconds = 4, + ... + }, + ), + ( + new(chrono::UTC, 0, 2021, 1, 15, 2, 3, 2, 2)!, + new(chrono::UTC, 0, 2021, 1, 16, 1, 1, 2, 4)!, + period { + hours = 22, + minutes = 58, + nanoseconds = 2, + ... + }, + ), + ( + new(chrono::UTC, 0, 500, 1, 1, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 3500, 1, 1, 0, 6, 0, 0)!, + period { + years = 3000, + minutes = 6, + ... + }, + ), + ( + new(chrono::UTC, 0, -500, 1, 1, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 2500, 1, 1, 0, 6, 0, 0)!, + period { + years = 3000, + minutes = 6, + ... + }, + ), + ( + new(chrono::UTC, 0, 2000, 1, 1, 0, 0, 0, 0)!, + new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!, + period { + minutes = 6, + nanoseconds = 999999999, + ... + }, + ), + ( + new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!, + new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 1, 0)!, + period { + nanoseconds = 1, + ... + }, + ), + ( + new(chrono::UTC, 0, -4000, 1, 1, 0, 6, 0, 999999999)!, + new(chrono::UTC, 0, 4000, 1, 1, 0, 6, 1, 0)!, + period { + years = 8000, + nanoseconds = 1, + ... + }, + ), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const da = cases[i].0; + const db = cases[i].1; + const expected = cases[i].2; + const actual = pdiff(da, db); + 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)!, + (27, 328, 1427, 9993, 239834, 14390073, 863404409i64, + (863404409i64 * time::SECOND) + 18), + ), + ( + 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)!, + (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)!, + (2500, 30000, 130443, 913106, 913106 * 24 + 22, + (913106 * 24 + 22) * 60 + 2, + ((913106 * 24 + 22) * 60 + 2) * 60i64, + (((913106 * 24 + 22) * 60 + 2) * 60i64 * + time::SECOND)), + ), + ]; + for (let i = 0z; i < len(cases); i += 1) { + const da = cases[i].0; + const db = cases[i].1; + const expected = cases[i].2; + assert(unitdiff(da, db, unit::YEAR) == expected.0, + "invalid diff_in_years() result"); + assert(unitdiff(da, db, unit::MONTH) == expected.1, + "invalid diff_in_months() result"); + assert(unitdiff(da, db, unit::WEEK) == expected.2, + "invalid diff_in_weeks() result"); + assert(unitdiff(da, db, unit::DAY) == expected.3, + "invalid diff_in_days() result"); + assert(unitdiff(da, db, unit::HOUR) == expected.4, + "invalid diff_in_hours() result"); + assert(unitdiff(da, db, unit::MINUTE) == expected.5, + "invalid diff_in_minutes() result"); + assert(unitdiff(da, db, unit::SECOND) == expected.6, + "invalid diff_in_seconds() result"); + assert(unitdiff(da, db, unit::NANOSECOND) == expected.7, + "invalid diff_in_nanoseconds() result"); + }; +}; + +@test fn truncate() void = { + const d = new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!; + + assert(chrono::eq( + &truncate(d, unit::ERA), + &new(chrono::UTC, 0, 1, 1, 1, 0, 0, 0, 0)!)!, + "invalid truncate() result 01"); + + assert(chrono::eq( + &truncate(d, unit::YEAR), + &new(chrono::UTC, 0, 1994, 1, 1, 0, 0, 0, 0)!)!, + "invalid truncate() result 02"); + + assert(chrono::eq( + &truncate(d, unit::MONTH), + &new(chrono::UTC, 0, 1994, 8, 1, 0, 0, 0, 0)!)!, + "invalid truncate() result 03"); + + assert(chrono::eq( + &truncate(d, unit::WEEK), + &new(chrono::UTC, 0, 1994, 8, 22, 0, 0, 0, 0)!)!, + "invalid truncate() result 04"); + + assert(chrono::eq( + &truncate(d, unit::DAY), + &new(chrono::UTC, 0, 1994, 8, 27, 0, 0, 0, 0)!)!, + "invalid truncate() result 05"); + + assert(chrono::eq( + &truncate(d, unit::HOUR), + &new(chrono::UTC, 0, 1994, 8, 27, 11, 0, 0, 0)!)!, + "invalid truncate() result 06"); + + assert(chrono::eq( + &truncate(d, unit::MINUTE), + &new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 0, 0)!)!, + "invalid truncate() result 07"); + + assert(chrono::eq( + &truncate(d, unit::SECOND), + &new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!)!, + "invalid truncate() result 08"); + + assert(chrono::eq( + &truncate(d, unit::NANOSECOND), + &d)!, + "invalid truncate() result 09"); +}; diff --git a/time/date/parse.ha b/time/date/parse.ha new file mode 100644 index 0000000..91df4a4 --- /dev/null +++ b/time/date/parse.ha @@ -0,0 +1,499 @@ +// License: MPL-2.0 +// (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 time; +use time::chrono; + +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. +// +// date::parse(&v, "%Y-%m-%d", "2019-12-27"); +// date::parse(&v, "%H:%M:%S.%N", "22:07:08.000000000"); +// date::parse(&v, "%z %Z %L", "+0100 CET Europe/Amsterdam"); +// +// Parse will return parsefail, if an invalid format specifier is encountered +// or if given string 's' does not match the layout. +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) { + const lr: rune = match (strings::next(&liter)) { + case void => + break; + case let lr: rune => + yield lr; + }; + + if (!escaped && lr == '%') { + escaped = true; + continue; + }; + + if (!escaped) { + const sr = match (strings::next(&siter)) { + case void => + return '\x00'; + case let sr: rune => + yield sr; + }; + if (sr != lr) { + return '\x00'; + }; + continue; + }; + + escaped = false; + + 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', 'e' => + v.day = scan_int(iter, 2)?; + case 'F' => + v.year = scan_int(iter, 4)?; + eat_rune(iter, '-')?; + v.month = scan_int(iter, 2)?; + eat_rune(iter, '-')?; + v.day = scan_int(iter, 2)?; + case 'H' => + v.hour = scan_int(iter, 2)?; + case 'I' => + v.halfhour = scan_int(iter, 2)?; + case 'j' => + v.yearday = scan_int(iter, 3)?; + case 'L' => + v.locname = scan_str(iter)?; + case 'm' => + v.month = scan_int(iter, 2)?; + case 'M' => + v.minute = scan_int(iter, 2)?; + case 'N' => + v.nanosecond = scan_decimal(iter, 9)?; + case 'p' => // AM=false PM=true + v.ampm = scan_for(iter, "AM", "PM", "am", "pm")? % 2 == 1; + case 'S' => + v.second = scan_int(iter, 2)?; + case 'T' => + v.hour = scan_int(iter, 2)?; + eat_rune(iter, ':')?; + v.minute = scan_int(iter, 2)?; + eat_rune(iter, ':')?; + v.second = scan_int(iter, 2)?; + case 'u' => + v.weekday = scan_int(iter, 1)? - 1; + case 'U' => + v.week = scan_int(iter, 2)?; + case 'w' => + v.weekday = scan_int(iter, 1)? - 1; + case 'W' => + v.week = scan_int(iter, 2)?; + case 'Y' => + v.year = scan_int(iter, 4)?; + case 'z' => + v.zoff = scan_zo(iter)?; + case 'Z' => + v.zabbr = scan_str(iter)?; + case '%' => + eat_rune(iter, '%')?; + case => + return failure; + }; +}; + +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); + }; + return i: int; + }; + }; + return failure; +}; + +// Scans the iterator for consecutive numeric digits. +// Left-padded whitespace and zeros are permitted. +// Returns the resulting int. +fn scan_int(iter: *strings::iterator, maxrunes: size) (int | failure) = { + let start = *iter; + let startfixed = false; + for (let i = 0z; i < maxrunes; i += 1) { + let rn: rune = match (strings::next(iter)) { + case void => + break; + case let rn: rune => + yield rn; + }; + if (!ascii::isdigit(rn) && rn != ' ') { + return failure; + }; + if (!startfixed) { + if (ascii::isdigit(rn)) { + startfixed = true; + } else { + strings::next(&start); + }; + };