diff --git a/.gitignore b/.gitignore index a44e550fb..d06e1951a 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,5 @@ TAGS # 'nix build' resulting symlink result +# macOS +.DS_Store \ No newline at end of file diff --git a/lib/std/time/datetime.c3 b/lib/std/time/datetime.c3 index f7c3cb534..30dbdf5e9 100644 --- a/lib/std/time/datetime.c3 +++ b/lib/std/time/datetime.c3 @@ -11,7 +11,7 @@ fn DateTime now() @require hour >= 0 && hour < 24 @require min >= 0 && min < 60 @require sec >= 0 && sec < 60 - @require us >= 0 && us < 999_999 + @require us >= 0 && us <= 999_999 *> fn DateTime from_date(int year, Month month = JANUARY, int day = 1, int hour = 0, int min = 0, int sec = 0, int us = 0) { @@ -25,7 +25,7 @@ fn DateTime from_date(int year, Month month = JANUARY, int day = 1, int hour = 0 @require hour >= 0 && hour < 24 @require min >= 0 && min < 60 @require sec >= 0 && sec < 60 - @require us >= 0 && us < 999_999 + @require us >= 0 && us <= 999_999 @require gmt_offset >= -12 * 3600 && gmt_offset <= 14 * 3600 *> fn TzDateTime from_date_tz(int year, Month month = JANUARY, int day = 1, int hour = 0, int min = 0, int sec = 0, int us = 0, int gmt_offset = 0) @@ -117,7 +117,7 @@ fn TzDateTime TzDateTime.to_gmt_offset(self, int gmt_offset) { @require hour >= 0 && hour < 24 @require min >= 0 && min <= 60 @require sec >= 0 && sec < 60 - @require us >= 0 && us < 999_999 + @require us >= 0 && us <= 999_999 *> fn void DateTime.set_date(&self, int year, Month month = JANUARY, int day = 1, int hour = 0, int min = 0, int sec = 0, int us = 0) { diff --git a/lib/std/time/format.c3 b/lib/std/time/format.c3 new file mode 100644 index 000000000..1c43f44ba --- /dev/null +++ b/lib/std/time/format.c3 @@ -0,0 +1,95 @@ +module std::time::datetime @if(env::LIBC); + + +enum DateTimeFormat +{ + ANSIC, // "Mon Jan _2 15:04:05 2006" + UNIXDATE, // "Mon Jan _2 15:04:05 GMT 2006" + RUBYDATE, // "Mon Jan 02 15:04:05 -0700 2006" + RFC822, // "02 Jan 06 15:04 GMT" + RFC822Z, // "02 Jan 06 15:04 -0700" + RFC850, // "Monday, 02-Jan-06 15:04:05 GMT" + RFC1123, // "Mon, 02 Jan 2006 15:04:05 GMT" + RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700" + RFC3339, // "2006-01-02T15:04:05Z" + RFC3339Z, // "2006-01-02T15:04:05+07:00" + RFC3339MS, // "2006-01-02T15:04:05.999999Z" + RFC3339ZMS, // "2006-01-02T15:04:05.999999+07:00" + DATETIME, // "2006-01-02 15:04:05" + DATEONLY, // "2006-01-02" + TIMEONLY, // "15:04:05" +} + +fn String format(DateTimeFormat type, TzDateTime dt, Allocator allocator) +{ + switch (type) + { + case ANSIC: + return string::format("%s %s %2d %02d:%02d:%02d %04d", dt.weekday.abbrev, dt.month.abbrev, dt.day, dt.hour, dt.min, dt.sec, dt.year, allocator: allocator); + case UNIXDATE: + return string::format("%s %s %2d %02d:%02d:%02d GMT %04d", dt.weekday.abbrev, dt.month.abbrev, dt.day, dt.hour, dt.min, dt.sec, dt.year, allocator: allocator); + case RUBYDATE: + return string::format("%s %s %2d %02d:%02d:%02d %s %04d", dt.weekday.abbrev, dt.month.abbrev, dt.day, dt.hour, dt.min, dt.sec, temp_numeric_tzsuffix(dt.gmt_offset), dt.year, allocator: allocator); + case RFC822: + dt = dt.to_gmt_offset(0); // For named representations of the timezone we always go for GMT, which is required by some RFCs + return string::format("%02d %s %02d %02d:%02d GMT", dt.day, dt.month.abbrev, dt.year % 100, dt.hour, dt.min, allocator: allocator); + case RFC822Z: + return string::format("%02d %s %02d %02d:%02d %s", dt.day, dt.month.abbrev, dt.year % 100, dt.hour, dt.min, temp_numeric_tzsuffix(dt.gmt_offset), allocator: allocator); + case RFC850: + dt = dt.to_gmt_offset(0); // For named representations of the timezone we always go for GMT, which is required by some RFCs + return string::format("%s, %02d-%s-%02d %02d:%02d:%02d GMT", dt.weekday.name, dt.day, dt.month.abbrev, dt.year % 100, dt.hour, dt.min, dt.sec, allocator: allocator); + case RFC1123: + dt = dt.to_gmt_offset(0); // For named representations of the timezone we always go for GMT, which is required by some RFCs + return string::format("%s, %02d %s %d %02d:%02d:%02d GMT", dt.weekday.abbrev, dt.day, dt.month.abbrev, dt.year, dt.hour, dt.min, dt.sec, allocator: allocator); + case RFC1123Z: + return string::format("%s, %02d %s %d %02d:%02d:%02d %s", dt.weekday.abbrev, dt.day, dt.month.abbrev, dt.year, dt.hour, dt.min, dt.sec, temp_numeric_tzsuffix(dt.gmt_offset), allocator: allocator); + case RFC3339: + dt = dt.to_gmt_offset(0); + return string::format("%04d-%02d-%02dT%02d:%02d:%02dZ", dt.year, dt.month + 1, dt.day, dt.hour, dt.min, dt.sec, allocator: allocator); + case RFC3339Z: + return string::format("%04d-%02d-%02dT%02d:%02d:%02d%s", dt.year, dt.month + 1, dt.day, dt.hour, dt.min, dt.sec, temp_numeric_tzsuffix_colon(dt.gmt_offset), allocator: allocator); + case RFC3339MS: + dt = dt.to_gmt_offset(0); + return string::format("%04d-%02d-%02dT%02d:%02d:%02d.%dZ", dt.year, dt.month + 1, dt.day, dt.hour, dt.min, dt.sec, dt.usec, allocator: allocator); + case RFC3339ZMS: + return string::format("%04d-%02d-%02dT%02d:%02d:%02d.%d%s", dt.year, dt.month + 1, dt.day, dt.hour, dt.min, dt.sec, dt.usec, temp_numeric_tzsuffix_colon(dt.gmt_offset), allocator: allocator); + case DATETIME: + return string::format("%04d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month + 1, dt.day, dt.hour, dt.min, dt.sec, allocator: allocator); + case DATEONLY: + return string::format("%04d-%02d-%02d", dt.year, dt.month + 1, dt.day, allocator: allocator); + case TIMEONLY: + return string::format("%02d:%02d:%02d", dt.hour, dt.min, dt.sec, allocator: allocator); + } +} + +fn String new_format(DateTimeFormat dt_format, TzDateTime dt) => format(dt_format, dt, allocator::heap()); +fn String temp_format(DateTimeFormat dt_format, TzDateTime dt) => format(dt_format, dt, allocator::temp()); + +fn String TzDateTime.format(self, DateTimeFormat dt_format, Allocator allocator) => format(dt_format, self, allocator); +fn String TzDateTime.new_format(self, DateTimeFormat dt_format) => format(dt_format, self, allocator::heap()); +fn String TzDateTime.temp_format(self, DateTimeFormat dt_format) => format(dt_format, self, allocator::temp()); + +// .with_gmt_offset(0) instead of .to_local() is used to avoid surprises when user is formatting to a representation without a timezone +fn String DateTime.format(self, DateTimeFormat dt_format, Allocator allocator) => format(dt_format, self.with_gmt_offset(0), allocator); +fn String DateTime.new_format(self, DateTimeFormat dt_format) => format(dt_format, self.with_gmt_offset(0), allocator::heap()); +fn String DateTime.temp_format(self, DateTimeFormat dt_format) => format(dt_format, self.with_gmt_offset(0), allocator::temp()); + +<* + Returns the timezone offset in the format of "+HHMM" or "-HHMM" + @require gmt_offset >= -12 * 3600 && gmt_offset <= 14 * 3600 +*> +fn String temp_numeric_tzsuffix(int gmt_offset) @private @inline +{ + if (gmt_offset == 0) return "-0000"; + return string::tformat("%+03d%02d", gmt_offset / 3600, (gmt_offset % 3600) / 60); +} + +<* + Returns the timezone offset in the format of "+HH:MM" or "-HH:MM" + @require gmt_offset >= -12 * 3600 && gmt_offset <= 14 * 3600 +*> +fn String temp_numeric_tzsuffix_colon(int gmt_offset) @private @inline +{ + if (gmt_offset == 0) return "-00:00"; + return string::tformat("%+03d:%02d", gmt_offset / 3600, (gmt_offset % 3600) / 60); +} \ No newline at end of file diff --git a/lib/std/time/time.c3 b/lib/std/time/time.c3 index 3d9f1cfb6..a15218212 100644 --- a/lib/std/time/time.c3 +++ b/lib/std/time/time.c3 @@ -46,34 +46,33 @@ struct TzDateTime int gmt_offset; } -enum Weekday : char +enum Weekday : char (String name, String abbrev) { - MONDAY, - TUESDAY, - WEDNESDAY, - THURSDAY, - FRIDAY, - SATURDAY, - SUNDAY, + MONDAY = { "Monday", "Mon" }, + TUESDAY = { "Tuesday", "Tue" }, + WEDNESDAY = { "Wednesday", "Wed" }, + THURSDAY = { "Thursday", "Thu" }, + FRIDAY = { "Friday", "Fri" }, + SATURDAY = { "Saturday", "Sat" }, + SUNDAY = { "Sunday", "Sun" }, } -enum Month : char +enum Month : char (String name, String abbrev, int days, bool leap) { - JANUARY, - FEBRUARY, - MARCH, - APRIL, - MAY, - JUNE, - JULY, - AUGUST, - SEPTEMBER, - OCTOBER, - NOVEMBER, - DECEMBER + JANUARY = { "January", "Jan", 31, false }, + FEBRUARY = { "February", "Feb", 28, true }, + MARCH = { "March", "Mar", 31, false }, + APRIL = { "April", "Apr", 30, false }, + MAY = { "May", "May", 31, false }, + JUNE = { "June", "Jun", 30, false }, + JULY = { "July", "Jul", 31, false }, + AUGUST = { "August", "Aug", 31, false }, + SEPTEMBER = { "September", "Sep", 30, false }, + OCTOBER = { "October", "Oct", 31, false }, + NOVEMBER = { "November", "Nov", 30, false }, + DECEMBER = { "December", "Dec", 31, false } } - fn Time now() { $if $defined(native_timestamp): diff --git a/test/unit/stdlib/time/format.c3 b/test/unit/stdlib/time/format.c3 new file mode 100644 index 000000000..fa1a68691 --- /dev/null +++ b/test/unit/stdlib/time/format.c3 @@ -0,0 +1,49 @@ +module timeformat_test @test; + +import std::time::datetime, std::collections::list, std::collections::triple; + +def FormatTzTestSpec = Triple(); +def FormatTestSpec = Triple(); + +fn void test_with_tz() +{ + FormatTzTestSpec[*] tests = { + { datetime::from_date(1970, Month.JANUARY, 1, 0, 0, 0).with_gmt_offset(0), RFC1123, "Thu, 01 Jan 1970 00:00:00 GMT" }, + { datetime::from_date(1994, Month.from_ordinal(10), 6, 8, 49, 37).with_gmt_offset(0), RFC1123, "Sun, 06 Nov 1994 08:49:37 GMT" }, + { datetime::from_date(2020, Month.JANUARY, 1, 0, 0, 0).with_gmt_offset(0), RFC1123, "Wed, 01 Jan 2020 00:00:00 GMT" }, + { datetime::from_date(2020, Month.JANUARY, 1, 0, 0, 0).with_gmt_offset(-3600), RFC1123, "Wed, 01 Jan 2020 01:00:00 GMT" }, + { datetime::from_date(2020, Month.JANUARY, 1, 0, 0, 0).with_gmt_offset(-3600), RFC1123Z, "Wed, 01 Jan 2020 00:00:00 -0100" }, + { datetime::from_date(2020, Month.JANUARY, 1, 0, 0, 0).with_gmt_offset(0), RFC1123Z, "Wed, 01 Jan 2020 00:00:00 -0000" }, + { datetime::from_date(2020, Month.JANUARY, 1, 0, 0, 0).with_gmt_offset(0), RFC3339, "2020-01-01T00:00:00Z" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 5, 999998).with_gmt_offset(0), RFC3339MS, "2006-01-02T15:04:05.999998Z" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 5, 999998).with_gmt_offset(25200), RFC3339Z, "2006-01-02T15:04:05+07:00" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 5, 999998).with_gmt_offset(25200), RFC3339ZMS, "2006-01-02T15:04:05.999998+07:00" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05).with_gmt_offset(0), RFC822, "02 Jan 06 15:04 GMT" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05).with_gmt_offset(0), RFC822Z, "02 Jan 06 15:04 -0000" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05).with_gmt_offset(0), RFC850, "Monday, 02-Jan-06 15:04:05 GMT" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05).with_gmt_offset(0), UNIXDATE, "Mon Jan 2 15:04:05 GMT 2006" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05).to_gmt_offset(0), RUBYDATE, "Mon Jan 2 15:04:05 -0000 2006" }, + }; + + foreach (test : tests) + { + String candidate = test.first.new_format(test.second); + assert(candidate == test.third, "got: '%s', expected: '%s'", candidate, test.third); + } +} + +fn void test_without_tz() +{ + FormatTestSpec[*] tests = { + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05), ANSIC, "Mon Jan 2 15:04:05 2006" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05), DATETIME, "2006-01-02 15:04:05" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05), DATEONLY, "2006-01-02" }, + { datetime::from_date(2006, Month.JANUARY, 2, 15, 4, 05), TIMEONLY, "15:04:05" }, + }; + + foreach (test : tests) + { + String candidate = test.first.new_format(test.second); + assert(candidate == test.third, "got: '%s', expected: '%s'", candidate, test.third); + } +}