commit 60e8b268408809c78b79decfcc213a5215daffca
parent 9ddc17e36545a01d9eadff121104322f2595a1f3
Author: Vlad-Stefan Harbuz <vlad@vladh.net>
Date: Fri, 14 Jan 2022 15:36:48 +0100
add long date format options e.g. {year}
Signed-off-by: Vlad-Stefan Harbuz <vlad@vladh.net>
Diffstat:
M | datetime/format.ha | | | 498 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- |
1 file changed, 364 insertions(+), 134 deletions(-)
diff --git a/datetime/format.ha b/datetime/format.ha
@@ -385,10 +385,230 @@ export fn format(layout: str, dt: *datetime) (str | errors::invalid | io::error)
return strio::string(&sink);
};
+fn fmt_short(r: rune, dt: *datetime) (str | errors::invalid | io::error) = {
+ return switch (r) {
+ case 'a' =>
+ // TODO: Localization
+ yield WEEKDAYS_SHORT[weekday(dt) - 1];
+ case 'A' =>
+ // TODO: Localization
+ yield WEEKDAYS[weekday(dt) - 1];
+ case 'b', 'h' =>
+ // TODO: Localization
+ yield MONTHS_SHORT[month(dt) - 1];
+ case 'B' =>
+ // TODO: Localization
+ yield MONTHS[month(dt) - 1];
+ case 'c' =>
+ // TODO: Localization
+ yield format("%a %b %e %H:%M:%S %Y", dt)?;
+ case 'C' =>
+ yield strconv::itos(year(dt) / 100);
+ case 'D' =>
+ yield format("%m/%d/%y", dt)?;
+ case 'd' =>
+ yield fmt::asprintf("{:02}", day(dt));
+ case 'e' =>
+ yield fmt::asprintf("{:2}", day(dt));
+ case 'F' =>
+ yield format("%Y-%m-%d", dt)?;
+ case 'g' =>
+ let year_str = strconv::itos(isoweekyear(dt));
+ yield strings::sub(year_str, len(year_str) - 2, strings::end);
+ case 'G' =>
+ yield strconv::itos(isoweekyear(dt));
+ case 'H' =>
+ yield fmt::asprintf("{:02}", hour(dt));
+ case 'I' =>
+ yield fmt::asprintf("{:02}", hour12(dt));
+ case 'j' =>
+ yield strconv::itos(yearday(dt));
+ case 'k' =>
+ yield strconv::itos(hour(dt));
+ case 'l' =>
+ yield strconv::itos(hour12(dt));
+ case 'm' =>
+ yield fmt::asprintf("{:02}", month(dt));
+ case 'M' =>
+ yield fmt::asprintf("{:02}", min(dt));
+ case 'n' =>
+ yield "\n";
+ case 'N' =>
+ yield fmt::asprintf("{:09}", strconv::itos(nsec(dt)));
+ case 'p' =>
+ // TODO: Localization
+ yield if (hour(dt) < 12) {
+ yield "AM";
+ } else {
+ yield "PM";
+ };
+ case 'P' =>
+ // TODO: Localization
+ yield if (hour(dt) < 12) {
+ yield "am";
+ } else {
+ yield "pm";
+ };
+ case 'r' =>
+ // TODO: Localization
+ yield format("%I:%M:%S %p", dt)?;
+ case 'R' =>
+ yield format("%H:%M", dt)?;
+ case 'S' =>
+ yield fmt::asprintf("{:02}", sec(dt));
+ case 't' =>
+ yield "\t";
+ case 'T' =>
+ yield format("%H:%M:%S", dt)?;
+ case 'u' =>
+ yield strconv::itos(weekday(dt));
+ case 'U' =>
+ // yield fmt::asprintf("{:02}", week_starting_sunday(dt));
+ // TODO
+ yield "";
+ case 'V' =>
+ yield fmt::asprintf("{:02}", isoweek(dt));
+ case 'w' =>
+ yield strconv::itos(weekday(dt) % 7);
+ case 'W' =>
+ yield fmt::asprintf("{:02}", week(dt));
+ case 'x' =>
+ // TODO: Localization
+ yield format("%m/%d/%y", dt)?;
+ case 'X' =>
+ // TODO: Localization
+ yield format("%H:%M:%S", dt)?;
+ case 'y' =>
+ let year_str = strconv::itos(year(dt));
+ yield strings::sub(year_str, len(year_str) - 2, strings::end);
+ case 'Y' =>
+ yield strconv::itos(year(dt));
+ case 'z' =>
+ //yield get_tz_hhmm(dt);
+ // TODO
+ yield "";
+ case 'Z' =>
+ //yield get_tz_name(dt);
+ // TODO
+ yield "";
+ case '%' =>
+ yield "%";
+ case =>
+ // Pass-through invalid conversion specifier
+ // characters.
+ const passthrough = strio::dynamic();
+ strio::appendrune(&passthrough, '%')!;
+ strio::appendrune(&passthrough, r)!;
+ yield strio::string(&passthrough);
+ };
+};
+
+fn fmt_long(
+ iter: *strings::iterator,
+ dt: *datetime
+) (str | errors::invalid | io::error) = {
+ const rest = strings::iter_str(iter);
+ const closing_idx = match (strings::index(rest, '}')) {
+ case let s: size =>
+ yield s;
+ case void =>
+ return errors::invalid;
+ };
+ const specifier = strings::sub(rest, 0, closing_idx);
+ eat_runes_until(iter, '}')?;
+
+ return switch (specifier) {
+ case "year" =>
+ yield strconv::itos(year(dt));
+ case "year_short" =>
+ let year_str = strconv::itos(year(dt));
+ yield strings::sub(year_str, len(year_str) - 2, strings::end);
+ case "month" =>
+ yield fmt::asprintf("{:02}", month(dt));
+ case "monthname" =>
+ // TODO: Localization
+ yield MONTHS[month(dt) - 1];
+ case "monthname_short" =>
+ // TODO: Localization
+ yield MONTHS_SHORT[month(dt) - 1];
+ case "week" =>
+ yield fmt::asprintf("{:02}", week(dt));
+ case "day" =>
+ yield fmt::asprintf("{:02}", day(dt));
+ case "yearday" =>
+ yield strconv::itos(yearday(dt));
+ case "weekday" =>
+ // TODO: Localization
+ yield WEEKDAYS[weekday(dt) - 1];
+ case "weekday_short" =>
+ // TODO: Localization
+ yield WEEKDAYS_SHORT[weekday(dt) - 1];
+ case "weekday_number" =>
+ yield strconv::itos(weekday(dt));
+ case "hour" =>
+ yield fmt::asprintf("{:02}", hour(dt));
+ case "hour_12" =>
+ yield fmt::asprintf("{:02}", hour12(dt));
+ case "minute", "min" =>
+ yield fmt::asprintf("{:02}", min(dt));
+ case "second", "sec" =>
+ yield fmt::asprintf("{:02}", sec(dt));
+ case "nanosecond", "nsec" =>
+ yield fmt::asprintf("{:09}", strconv::itos(nsec(dt)));
+ case "ampm" =>
+ // TODO: Localization
+ yield if (hour(dt) < 12) {
+ yield "am";
+ } else {
+ yield "pm";
+ };
+ case "ampm_upper" =>
+ // TODO: Localization
+ yield if (hour(dt) < 12) {
+ yield "AM";
+ } else {
+ yield "PM";
+ };
+ case "time_hm" =>
+ yield format("%H:%M", dt)?;
+ case "time" =>
+ yield format("%H:%M:%S", dt)?;
+ case "date" =>
+ yield format("%d/%m/%Y", dt)?;
+ case "date_iso" =>
+ yield format("%Y-%m-%d", dt)?;
+ case "century" =>
+ yield strconv::itos(year(dt) / 100);
+ case "rfc3999" =>
+ yield format(RFC3999, dt)?;
+ case "email" =>
+ yield format(EMAIL, dt)?;
+ case "posix" =>
+ yield format(POSIX, dt)?;
+ case "stamp" =>
+ yield format(STAMP, dt)?;
+ case "stamp_nano" =>
+ yield format(STAMP_NANO, dt)?;
+ case =>
+ // Pass-through invalid conversion specifier
+ // characters.
+ const passthrough = strio::dynamic();
+ strio::appendrune(&passthrough, '{')!;
+ strio::concat(&passthrough, specifier)!;
+ strio::appendrune(&passthrough, '}')!;
+ yield strio::string(&passthrough);
+
+ };
+};
+
// Formats a [[datetime]] and writes to an [[io::handle]].
-export fn fmtstream(h: io::handle, layout: str, dt: *datetime) (size | errors::invalid | io::error) = {
+export fn fmtstream(
+ h: io::handle,
+ layout: str,
+ dt: *datetime
+) (size | errors::invalid | io::error) = {
const iter = strings::iter(layout);
- let escaped = false;
+ let short_escaped = false;
let n = 0z;
for (true) {
let r: rune = match (strings::next(&iter)) {
@@ -398,141 +618,20 @@ export fn fmtstream(h: io::handle, layout: str, dt: *datetime) (size | errors::i
yield r;
};
- if (!escaped && r == '%') {
- escaped = true;
- continue;
- };
-
- if (!escaped) {
- strio::appendrune(h, r)?;
- continue;
- };
-
- escaped = false;
- let s = switch (r) {
- case 'a' =>
- // TODO: Localization
- yield WEEKDAYS_SHORT[weekday(dt) - 1];
- case 'A' =>
- // TODO: Localization
- yield WEEKDAYS[weekday(dt) - 1];
- case 'b', 'h' =>
- // TODO: Localization
- yield MONTHS_SHORT[month(dt) - 1];
- case 'B' =>
- // TODO: Localization
- yield MONTHS[month(dt) - 1];
- case 'c' =>
- // TODO: Localization
- yield format("%a %b %e %H:%M:%S %Y", dt)?;
- case 'C' =>
- yield strconv::itos(year(dt) / 100);
- case 'D' =>
- yield format("%m/%d/%y", dt)?;
- case 'd' =>
- yield fmt::asprintf("{:02}", day(dt));
- case 'e' =>
- yield fmt::asprintf("{:2}", day(dt));
- case 'F' =>
- yield format("%Y-%m-%d", dt)?;
- case 'g' =>
- let year_str = strconv::itos(isoweekyear(dt));
- yield strings::sub(year_str,
- len(year_str) - 2, strings::end);
- case 'G' =>
- yield strconv::itos(isoweekyear(dt));
- case 'H' =>
- yield fmt::asprintf("{:02}", hour(dt));
- case 'I' =>
- let mod_hour = hour(dt) % 12;
- if (mod_hour == 0) {
- mod_hour = 12;
- };
- yield fmt::asprintf("{:02}", mod_hour);
- case 'j' =>
- yield strconv::itos(yearday(dt));
- case 'k' =>
- yield strconv::itos(hour(dt));
- case 'l' =>
- let mod_hour = hour(dt) % 12;
- if (mod_hour == 0) {
- mod_hour = 12;
- };
- yield strconv::itos(mod_hour);
- case 'm' =>
- yield fmt::asprintf("{:02}", month(dt));
- case 'M' =>
- yield fmt::asprintf("{:02}", min(dt));
- case 'n' =>
- yield "\n";
- case 'N' =>
- yield fmt::asprintf("{:09}", strconv::itos(nsec(dt)));
- case 'p' =>
- // TODO: Localization
- yield if (hour(dt) < 12) {
- yield "AM";
- } else {
- yield "PM";
- };
- case 'P' =>
- // TODO: Localization
- yield if (hour(dt) < 12) {
- yield "am";
+ if (r == '{') {
+ n += strio::concat(h, fmt_long(&iter, dt)?)?;
+ } else {
+ if (short_escaped) {
+ short_escaped = false;
+ n += strio::concat(h, fmt_short(r, dt)?)?;
} else {
- yield "pm";
+ if (r == '%') {
+ short_escaped = true;
+ } else {
+ strio::appendrune(h, r)?;
+ };
};
- case 'r' =>
- // TODO: Localization
- yield format("%I:%M:%S %p", dt)?;
- case 'R' =>
- yield format("%H:%M", dt)?;
- case 'S' =>
- yield fmt::asprintf("{:02}", sec(dt));
- case 't' =>
- yield "\t";
- case 'T' =>
- yield format("%H:%M:%S", dt)?;
- case 'u' =>
- yield strconv::itos(weekday(dt));
- case 'U' =>
- // yield fmt::asprintf("{:02}", week_starting_sunday(dt));
- // TODO
- yield "";
- case 'V' =>
- yield fmt::asprintf("{:02}", isoweek(dt));
- case 'w' =>
- yield strconv::itos(weekday(dt) % 7);
- case 'W' =>
- yield fmt::asprintf("{:02}", week(dt));
- case 'x' =>
- // TODO: Localization
- yield format("%m/%d/%y", dt)?;
- case 'X' =>
- // TODO: Localization
- yield format("%H:%M:%S", dt)?;
- case 'y' =>
- let year_str = strconv::itos(year(dt));
- yield strings::sub(year_str,
- len(year_str) - 2, strings::end);
- case 'Y' =>
- yield strconv::itos(year(dt));
- case 'z' =>
- //yield get_tz_hhmm(dt);
- // TODO
- yield "";
- case 'Z' =>
- //yield get_tz_name(dt);
- // TODO
- yield "";
- case '%' =>
- yield "%";
- case =>
- // Pass-through invalid conversion specifier characters.
- strio::appendrune(h, '%')?;
- strio::appendrune(h, r)?;
- continue;
};
- n += strio::concat(h, s)?;
};
return n;
};
@@ -598,6 +697,23 @@ fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | errors::invalid)
};
};
+fn eat_runes_until(iter: *strings::iterator, needle: rune) (uint | errors::invalid) = {
+ let n: uint = 0u;
+ for (true) {
+ let s_r = match (strings::next(iter)) {
+ case void =>
+ return errors::invalid;
+ case let r: rune =>
+ yield r;
+ };
+ n += 1;
+ if (s_r == needle) {
+ break;
+ };
+ };
+ return n;
+};
+
fn clamp_int(i: int, min: int, max: int) int = {
return if (i < min) {
yield min;
@@ -608,6 +724,120 @@ fn clamp_int(i: int, min: int, max: int) int = {
};
};
+fn hour12(dt: *datetime) int = {
+ let mod_hour = hour(dt) % 12;
+ if (mod_hour == 0) {
+ mod_hour = 12;
+ };
+ return mod_hour;
+};
+
+@test fn format() void = {
+ const dt = new(1994, 01, 01, 02, 17, 05, 24, 0, chrono::local)!;
+
+ const cases = [
+ // special characters
+ ("%t", "\t"),
+ ("%n", "\n"),
+ ("%%", "%"),
+ // hour
+ ("%H", "02"),
+ ("%k", "2"),
+ ("%I", "02"),
+ ("%l", "2"),
+ ("{hour}", "02"),
+ ("{hour_12}", "02"),
+ // minute
+ ("%M", "17"),
+ ("{minute}", "17"),
+ // second
+ ("%S", "05"),
+ ("{second}", "05"),
+ // nanosecond
+ ("%N", "000000024"),
+ ("{nanosecond}", "000000024"),
+ // am/pm
+ ("%p", "AM"),
+ ("{ampm_upper}", "AM"),
+ ("%P", "am"),
+ ("{ampm}", "am"),
+ // day
+ ("%d", "01"),
+ ("%e", " 1"),
+ ("{day}", "01"),
+ // month
+ ("%m", "01"),
+ ("{month}", "01"),
+ // year
+ ("%Y", "1994"),
+ ("{year}", "1994"),
+ ("%y", "94"),
+ ("{year_short}", "94"),
+ // month name
+ ("%b", "Jan"),
+ ("%h", "Jan"),
+ ("{monthname_short}", "Jan"),
+ ("%B", "January"),
+ ("{monthname}", "January"),
+ // weekday
+ ("%u", "6"),
+ ("%w", "6"),
+ ("{weekday_number}", "6"),
+ ("%a", "Sat"),
+ ("{weekday_short}", "Sat"),
+ ("%A", "Saturday"),
+ ("{weekday}", "Saturday"),
+ // yearday
+ ("%j", "1"),
+ ("{yearday}", "1"),
+ // week
+ ("%W", "00"),
+ ("{week}", "00"),
+ // isoweek
+ ("%V", "01"),
+ // isoyear
+ ("%G", "1993"),
+ ("%g", "93"),
+ // time
+ ("%R", "02:17"),
+ ("{time_hm}", "02:17"),
+ ("%T", "02:17:05"),
+ ("%X", "02:17:05"),
+ ("{time}", "02:17:05"),
+ ("%r", "02:17:05 AM"),
+ // datetime
+ ("%c", "Sat Jan 1 02:17:05 1994"),
+ // date
+ ("%x", "01/01/94"),
+ ("%D", "01/01/94"),
+ ("{date}", "01/01/1994"),
+ ("%F", "1994-01-01"),
+ ("{date_iso}", "1994-01-01"),
+ // century
+ ("%C", "19"),
+ ("{century}", "19"),
+ // standards
+ ("{rfc3999}", "1994-01-01T02:17:05"),
+ ("{email}", "Sat, 01 Jan 1994 02:17:05 "),
+ ("{posix}", "Sat Jan 1 02:17:05 1994"),
+ ("{stamp}", "1994-01-01 02:17:05"),
+ ("{stamp_nano}", "1994-01-01 02:17:05.000000024"),
+ ];
+
+ for (let i = 0z; i < len(cases); i += 1) {
+ const layout = cases[i].0;
+ const expected = cases[i].1;
+ const actual = format(layout, &dt)!;
+ if (actual != expected) {
+ fmt::printfln(
+ "expected format({}, &dt) to be {} but was {}",
+ layout, expected, actual
+ )!;
+ abort();
+ };
+ };
+};
+
// TODO: Refactor this once the rest of the parse() refactoring is done
// @test fn parse() void = {
// let dt = datetime {...};