Skip to content

Commit

Permalink
Remove %zone slot from <time>
Browse files Browse the repository at this point in the history
There's no need for each `<time>` instance to contain a zone and since `<time>`
objects may be created frequently we don't want unnecessary slots taking up
space. Instead, the zone should always be specified when displaying times.

Removed the `zone:` keyword argument from `time-now`.

`compose-time` and `time-components` now accept a `zone:` keyword argument
with which to interpret the components.

Removed all of the component readers (`time-year`, `time-month`, etc.); just
use `time-components` directly.

Removed `time-in-zone` which no longer made sense.

Also marked `test-us-eastern-sanity-check` as expected to fail for now.
  • Loading branch information
cgay committed Oct 23, 2024
1 parent 2021d6d commit 62e6c21
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 236 deletions.
18 changes: 9 additions & 9 deletions sources/formatting-test.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,20 @@ define test test-rfc3339-format ()
// Verify that negative zone offset overflow displays as previous day.
assert-equal("1969-12-31T19:00:00.000000-05:00",
with-output-to-string (s)
let t = time-in-zone($epoch, make(<naive-zone>,
offset-seconds: -5 * 60 * 60,
name: "x"));
format-time(s, $rfc3339-microseconds, t)
format-time(s, $rfc3339-microseconds, $epoch,
zone: make(<naive-zone>,
offset-seconds: -5 * 60 * 60,
name: "x"))
end);
// Verify that positive zone offset overflow displays as next day.
// (Throw in a test for leap day Feb 29 because why not.)
assert-equal("2020-02-29T00:00:00.000000+05:00",
with-output-to-string (s)
let t = time-in-zone(compose-time(2020, $february, 28, 19, 0, 0, 0, $utc),
make(<naive-zone>,
offset-seconds: 5 * 60 * 60,
name: "x"));
format-time(s, $rfc3339-microseconds, t)
let t = compose-time(2020, $february, 28, 19, 0, 0, 0);
format-time(s, $rfc3339-microseconds, t,
zone: make(<naive-zone>,
offset-seconds: 5 * 60 * 60,
name: "x"))
end);
end test;

Expand Down
102 changes: 55 additions & 47 deletions sources/formatting.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -72,54 +72,53 @@ define function parse-time-format (descriptor :: <string>) => (_ :: <sequence>)
end function;

// Each value is a pair of #(time-component . formatter-function) where
// time-component is the index into the return values list of the
// time-components function. 0 = year, 1 = month, etc
// time-component is a keyword that matches the select statement used in
// format-time.
//
// TODO: BC/AD, BCE/CE (see ISO 8601)
// TODO: make this extensible
define table $time-format-map :: <string-table>
= { "yyyy" => pair(0, curry(format-ndigit-int, 4)),
"yy" => pair(0, curry(format-ndigit-int-mod, 2, 100)),
"mm" => pair(1, method (stream, month)
format-ndigit-int(2, stream, month.month-number)
end),
"mon" => pair(1, format-short-month-name),
"month" => pair(1, format-long-month-name),
"dd" => pair(2, curry(format-ndigit-int, 2)),
"HH" => pair(3, curry(format-ndigit-int, 2)),
"hh" => pair(3, format-hour-12),
"am" => pair(3, format-lowercase-am-pm),
"pm" => pair(3, format-lowercase-am-pm),
"AM" => pair(3, format-uppercase-am-pm),
"PM" => pair(3, format-uppercase-am-pm),
"MM" => pair(4, curry(format-ndigit-int, 2)),
"SS" => pair(5, curry(format-ndigit-int, 2)),
"millis" => pair(6, curry(format-ndigit-int-mod, 3, 1000)),
"micros" => pair(6, curry(format-ndigit-int-mod, 6, 1_000_000)),
"nanos" => pair(6, curry(format-ndigit-int-mod, 9, 1_000_000_000)),
= { "yyyy" => pair(#"year", curry(format-ndigit-int, 4)),
"yy" => pair(#"year", curry(format-ndigit-int-mod, 2, 100)),
"mm" => pair(#"month", method (stream, month)
format-ndigit-int(2, stream, month.month-number)
end),
"mon" => pair(#"month", format-short-month-name),
"month" => pair(#"month", format-long-month-name),
"dd" => pair(#"day-of-month", curry(format-ndigit-int, 2)),
"HH" => pair(#"hour", curry(format-ndigit-int, 2)),
"hh" => pair(#"hour", format-hour-12),
"am" => pair(#"hour", format-lowercase-am-pm),
"pm" => pair(#"hour", format-lowercase-am-pm),
"AM" => pair(#"hour", format-uppercase-am-pm),
"PM" => pair(#"hour", format-uppercase-am-pm),
"MM" => pair(#"minute", curry(format-ndigit-int, 2)),
"SS" => pair(#"second", curry(format-ndigit-int, 2)),
"millis" => pair(#"nanosecond", curry(format-ndigit-int-mod, 3, 1000)),
"micros" => pair(#"nanosecond", curry(format-ndigit-int-mod, 6, 1_000_000)),
"nanos" => pair(#"nanosecond", curry(format-ndigit-int-mod, 9, 1_000_000_000)),
// f = fractional seconds with minimum digits. fN outputs exactly N digits.
"f" => pair(6, format-nanos-with-minimum-digits),
"f" => pair(#"nanosecond", format-nanos-with-minimum-digits),
// Not sure if some of these will be used, but might as well be complete.
"f1" => pair(6, curry(format-ndigit-int-mod, 1, 10)),
"f2" => pair(6, curry(format-ndigit-int-mod, 2, 100)),
"f3" => pair(6, curry(format-ndigit-int-mod, 3, 1000)),
"f4" => pair(6, curry(format-ndigit-int-mod, 4, 10_000)),
"f5" => pair(6, curry(format-ndigit-int-mod, 5, 100_000)),
"f6" => pair(6, curry(format-ndigit-int-mod, 6, 1_000_000)),
"f7" => pair(6, curry(format-ndigit-int-mod, 7, 10_000_000)),
"f8" => pair(6, curry(format-ndigit-int-mod, 8, 100_000_000)),
"f9" => pair(6, curry(format-ndigit-int-mod, 9, 1_000_000_000)),

"zone" => pair(7, format-zone-name), // UTC, PST, etc
"f1" => pair(#"nanosecond", curry(format-ndigit-int-mod, 1, 10)),
"f2" => pair(#"nanosecond", curry(format-ndigit-int-mod, 2, 100)),
"f3" => pair(#"nanosecond", curry(format-ndigit-int-mod, 3, 1000)),
"f4" => pair(#"nanosecond", curry(format-ndigit-int-mod, 4, 10_000)),
"f5" => pair(#"nanosecond", curry(format-ndigit-int-mod, 5, 100_000)),
"f6" => pair(#"nanosecond", curry(format-ndigit-int-mod, 6, 1_000_000)),
"f7" => pair(#"nanosecond", curry(format-ndigit-int-mod, 7, 10_000_000)),
"f8" => pair(#"nanosecond", curry(format-ndigit-int-mod, 8, 100_000_000)),
"f9" => pair(#"nanosecond", curry(format-ndigit-int-mod, 9, 1_000_000_000)),

"zone" => pair(#"zone", format-zone-name), // UTC, PST, etc

// TODO: these fail for <aware-zone>s. Need some refactoring to make sure the
// offset is found for the correct time.
"offset" => pair(7, rcurry(format-zone-offset, colon?: #f, utc-name: #f)), // +0000
"offset:" => pair(7, rcurry(format-zone-offset, colon?: #t, utc-name: #f)), // +00:00
"offset:Z" => pair(7, rcurry(format-zone-offset, colon?: #t, utc-name: "Z")), // Z or +02:00
"offset" => pair(#"zone", rcurry(format-zone-offset, colon?: #f, utc-name: #f)), // +0000
"offset:" => pair(#"zone", rcurry(format-zone-offset, colon?: #t, utc-name: #f)), // +00:00
"offset:Z" => pair(#"zone", rcurry(format-zone-offset, colon?: #t, utc-name: "Z")), // Z or +02:00

"day" => pair(8, format-short-weekday),
"weekday" => pair(8, format-long-weekday),
"day" => pair(#"day-of-week", format-short-weekday),
"weekday" => pair(#"day-of-wook", format-long-weekday),
};

define function format-nanos-with-minimum-digits
Expand Down Expand Up @@ -256,23 +255,32 @@ define /* inline */ method format-time
format-time(stream, parse-time-format(fmt), time, zone: zone);
end method;

define /* inline */ method format-time
define method format-time
(stream :: <stream>, fmt :: <sequence>, time :: <time>, #key zone :: <zone>?)
=> ()
// I'm assuming that v is stack allocated. Verify.
let (#rest v) = time-components(time);
if (zone)
v[7] := zone;
end;
let zone :: <zone> = zone | $utc;
let (year, month, day-of-month, hour, minute, second, nanosecond, day-of-week)
= time-components(time, zone: zone);
for (item in fmt)
select (item by instance?)
<string>
=> write(stream, item);
<pair>
=> begin
let index :: <integer> = item.head;
let value
= select (item.head)
#"year" => year;
#"month" => month;
#"day-of-month" => day-of-month;
#"hour" => hour;
#"minute" => minute;
#"second" => second;
#"nanosecond" => nanosecond;
#"day-of-week" => day-of-week;
#"zone" => zone;
end;
let formatter :: <function> = item.tail;
formatter(stream, v[index]);
formatter(stream, value);
end;
otherwise => time-error("invalid time format element: %=", item);
end;
Expand Down
12 changes: 1 addition & 11 deletions sources/library.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,7 @@ define module time
// Time
<time>,
time-now,
time-components, // returns the following nine values, in order
time-year,
time-month,
time-day-of-month,
time-hour,
time-minute,
time-second,
time-nanosecond,
time-zone,
time-day-of-week,
time-components, // => year, month, day, hour, minute, second, nanosecond
$epoch,
$minimum-time,
$maximum-time,
Expand Down Expand Up @@ -68,7 +59,6 @@ define module time
// Conversions
compose-time, // make a <time> from its components
time-components, // break a <time> into its components
time-in-zone,
parse-time, // TODO: $iso-8601-format etc?
parse-duration,
parse-day, // TODO: not sure about this
Expand Down
17 changes: 4 additions & 13 deletions sources/specification.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,13 @@ Module: time-test-suite
define interface-specification-suite time-specification-suite ()
// Time
sealed instantiable class <time> (<object>);
function time-now(#"key", #"zone") => (<time>);
function time-now() => (<time>);
sealed generic function compose-time
(<integer>, <month>, <integer>, <integer>, <integer>, <integer>, <integer>, <zone>)
(<integer>, <month>, <integer>, <integer>, <integer>, <integer>, <integer>, #"key", #"zone")
=> (<time>);
sealed generic function time-components
(<time>)
=> (<integer>, <month>, <integer>, <integer>, <integer>, <integer>, <integer>, <zone>, <day>);
sealed generic function time-year (<time>) => (<integer>);
sealed generic function time-month (<time>) => (<month>);
sealed generic function time-day-of-month (<time>) => (<integer>);
sealed generic function time-hour (<time>) => (<integer>);
sealed generic function time-minute (<time>) => (<integer>);
sealed generic function time-second (<time>) => (<integer>);
sealed generic function time-nanosecond (<time>) => (<integer>);
sealed generic function time-zone (<time>) => (<zone>);
sealed generic function time-day-of-week (<time>) => (<day>);
(<time>, #"key", #"zone")
=> (<integer>, <month>, <integer>, <integer>, <integer>, <integer>, <integer>, <day>);
constant $epoch :: <time>;

// Durations
Expand Down
83 changes: 44 additions & 39 deletions sources/time-test.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,50 @@ define test test-current-time ()
assert-true(t.%nanoseconds >= 0 & t.%nanoseconds < 1_000_000_000 * 60 * 60 * 24);
end test;

define constant $components-test-cases
= list(list(list(1970, $january, 1, 0, 0, 0, 0, $utc, $monday),
list(0, 0)),
list(list(1970, $january, 2, 1, 1, 1, 1, $utc, $monday),
list(1, 3_661_000_000_001)),
list(list(1969, $december, 31, 0, 0, 0, 1, $utc, $monday),
list(-1, 1)),
list(list(1969, $december, 31, 23, 59, 59, 999_999_999, $utc, $monday),
list(-1, 86_399_999_999_999)),
list(list(2020, $october, 18, 0, 0, 0, 0, $utc, $monday),
list(18553, 0)));

// TODO: test non-UTC zone
define test test-compose-time ()
for (tc in $components-test-cases)
let (args, want) = apply(values, tc);
let args = copy-sequence(args, end: args.size - 1); // remove the day
let t = apply(compose-time, args);
let (want-days, want-nanos) = apply(values, want);
assert-equal(t.%days, want-days,
format-to-string("for %= got days %=, want %=",
args, t.%days, want-days));
assert-equal(t.%nanoseconds, want-nanos,
format-to-string("for %= got nanoseconds %=, want %=",
args, t.%nanoseconds, want-nanos));
end;
let t1 = compose-time(1970, $january, 1, 0, 0, 0, 0, zone: $utc);
assert-equal(0, t1.%days);
assert-equal(0, t1.%nanoseconds);

let t2 = compose-time(1970, $january, 2, 1, 1, 1, 1, zone: $utc);
assert-equal(1, t2.%days);
assert-equal(3_661_000_000_001, t2.%nanoseconds);

let t3 = compose-time(1969, $december, 31, 0, 0, 0, 1, zone: $utc);
assert-equal(-1, t3.%days);
assert-equal(1, t3.%nanoseconds);

let t4 = compose-time(1969, $december, 31, 23, 59, 59, 999_999_999, zone: $utc);
assert-equal(-1, t4.%days);
assert-equal(86_399_999_999_999, t4.%nanoseconds);

let t5 = compose-time(2020, $october, 18, 0, 0, 0, 0, zone: $utc);
assert-equal(18553, t5.%days);
assert-equal(0, t5.%nanoseconds);
end test;

// TODO: test non-UTC zone
define test test-time-components ()
for (tc in $components-test-cases)
let (args, want) = apply(values, reverse(tc));
let t = make(<time>, days: args[0], nanoseconds: args[1]);
let (#rest got) = time-components(t);
assert-equal(got, want,
format-to-string("for %= got %=, want %=", args, got, want));
end;
let t1 = make(<time>, days: 0, nanoseconds: 0);
let (#rest c1) = time-components(t1, zone: $utc);
assert-equal(vector(1970, $january, 1, 0, 0, 0, 0, $monday), c1);

let t2 = make(<time>, days: 1, nanoseconds: 3_661_000_000_001);
let (#rest c2) = time-components(t2, zone: $utc);
assert-equal(vector(1970, $january, 2, 1, 1, 1, 1, $monday), c2);

let t3 = make(<time>, days: -1, nanoseconds: 1);
let (#rest c3) = time-components(t3, zone: $utc);
assert-equal(vector(1969, $december, 31, 0, 0, 0, 1, $monday), c3);

let t4 = make(<time>, days: -1, nanoseconds: 86_399_999_999_999);
let (#rest c4) = time-components(t4, zone: $utc);
assert-equal(vector(1969, $december, 31, 23, 59, 59, 999_999_999, $monday), c4);

let t5 = make(<time>, days: 18553, nanoseconds: 0);
let (#rest c5) = time-components(t5, zone: $utc);
assert-equal(vector(2020, $october, 18, 0, 0, 0, 0, $monday), c5);
end test;


Expand All @@ -66,14 +75,9 @@ define test test-time+duration ()
end test;

define test test-time-= ()
// Two times with the same UTC seconds and nanoseconds should be equal.
let t1 = time-now();
assert-equal(t1, make(<time>, days: t1.%days, nanoseconds: t1.%nanoseconds));

// Two times with the same UTC seconds and nanoseconds should be equal
// regardless of zone.
assert-equal(make(<time>, days: 1, nanoseconds: 1, zone: $utc),
make(<time>, days: 1, nanoseconds: 1,
zone: make(<naive-zone>, name: "x", offset-seconds: 5 * 60 * 60)));
end test;

define test test-time-< ()
Expand All @@ -87,6 +91,7 @@ end test;

define test test-print-object ()
assert-equal("1970-01-01T00:00:00.0Z", format-to-string("%s", $epoch));
assert-true(regex-search(compile-regex("{<time> 0d 0ns \\+00:00 \\d+}"),
format-to-string("%=", $epoch)));
assert-true(regex-search(compile-regex("{<time> 0d 0ns UTC \\d+}"),
format-to-string("%=", $epoch)),
"%= didn't match the regular expression", $epoch);
end test;
Loading

0 comments on commit 62e6c21

Please sign in to comment.