commit a6448dbd209d83e01c673cbca5c505418ebf91b4
parent 0e8bdbd0fd6da8394aa8337b876fec2e2757d836
Author: Vlad-Stefan Harbuz <vlad@vladh.net>
Date: Wed, 17 Nov 2021 22:41:25 +0100
add strptime()
Signed-off-by: Vlad-Stefan Harbuz <vlad@vladh.net>
Diffstat:
3 files changed, 532 insertions(+), 10 deletions(-)
diff --git a/datetime/format+test.ha b/datetime/format+test.ha
@@ -0,0 +1,139 @@
+use errors;
+use time::chrono;
+use fmt;
+
+@test fn strptime() void = {
+ let dt = datetime {...};
+
+ // General tests
+ strptime("%Y-%m-%d %H:%M:%S.%N", "1994-08-27 11:01:02.123", &dt)!;
+ assert(dt.date.year as int == 1994 &&
+ dt.date.month as int == 08 &&
+ dt.date.day as int == 27 &&
+ dt.time.hour as int == 11 &&
+ dt.time.min as int == 01 &&
+ dt.time.sec as int == 02 &&
+ dt.time.nsec as int == 123, "invalid parsing results");
+
+ strptime("%k:%M:%S.%N%n%t%%", " 9:01:02.123\n\t%", &dt)!;
+ assert(dt.time.hour as int == 9 &&
+ dt.time.min as int == 01 &&
+ dt.time.sec as int == 02 &&
+ dt.time.nsec as int == 123, "invalid parsing results");
+
+ strptime("%G-%m-%e", "994-8- 9", &dt)!;
+ assert(dt.date.isoweekyear as int == 994 &&
+ dt.date.month as int == 8 &&
+ dt.date.day as int == 9, "invalid parsing results");
+
+ // General errors
+ assert(strptime("%Y-%m-%d", "1a94-08-27", &dt) is errors::invalid,
+ "invalid datetime string did not throw error");
+
+ assert(strptime("%Y-%m-%d", "1994-123-27", &dt) is errors::invalid,
+ "invalid datetime string did not throw error");
+
+ assert(strptime("%Y-%m-%d", "a994-08-27", &dt) is errors::invalid,
+ "invalid datetime string did not throw error");
+
+ // Basic specifiers
+ strptime("%a", "Tue", &dt)!;
+ assert(dt.date.weekday as int == 2, "invalid parsing results");
+
+ strptime("%a %d", "Tue 27", &dt)!;
+ assert(dt.date.weekday as int == 2 &&
+ dt.date.day as int == 27, "invalid parsing results");
+
+ strptime("%A", "Tuesday", &dt)!;
+ assert(dt.date.weekday as int == 2, "invalid parsing results");
+
+ strptime("%b", "Feb", &dt)!;
+ assert(dt.date.month as int == 2, "invalid parsing results");
+
+ strptime("%h", "Feb", &dt)!;
+ assert(dt.date.month as int == 2, "invalid parsing results");
+
+ strptime("%B", "February", &dt)!;
+ assert(dt.date.month as int == 2, "invalid parsing results");
+
+ strptime("%I", "14", &dt)!;
+ assert(dt.time.hour as int == 2, "invalid parsing results");
+
+ strptime("%j", "123", &dt)!;
+ assert(dt.date.yearday as int == 123, "invalid parsing results");
+
+ strptime("%l", " 9", &dt)!;
+ assert(dt.time.hour as int == 9, "invalid parsing results");
+
+ strptime("%H %p", "6 AM", &dt)!;
+ assert(dt.time.hour as int == 6, "invalid parsing results");
+
+ strptime("%H %p", "6 PM", &dt)!;
+ assert(dt.time.hour as int == 18, "invalid parsing results");
+
+ assert(strptime("%H %p", "13 PM", &dt) is errors::invalid,
+ "invalid parsing results");
+
+ assert(strptime("%H %p", "PM 6", &dt) is errors::invalid,
+ "invalid parsing results");
+
+ strptime("%H %P", "6 am", &dt)!;
+ assert(dt.time.hour as int == 6, "invalid parsing results");
+
+ strptime("%u", "7", &dt)!;
+ assert(dt.date.weekday as int == 7, "invalid parsing results");
+
+ strptime("%U", "2", &dt)!;
+ assert(dt.date.week as int == 2, "invalid parsing results");
+
+ strptime("%U", "99", &dt)!;
+ assert(dt.date.week as int == 53, "invalid parsing results");
+
+ strptime("%V", "12", &dt)!;
+ assert(dt.date.isoweek as int == 12, "invalid parsing results");
+
+ strptime("%w", "0", &dt)!;
+ assert(dt.date.weekday as int == 7, "invalid parsing results");
+
+ strptime("%W", "2", &dt)!;
+ assert(dt.date.week as int == 2, "invalid parsing results");
+
+ // Expansion specifiers
+ strptime("%c", "Tue Feb 2 22:12:50 1994", &dt)!;
+ assert(dt.date.day as int == 2 &&
+ dt.date.month as int == 2 &&
+ dt.date.year as int == 1994 &&
+ dt.date.weekday as int == 2 &&
+ dt.time.hour as int == 22 &&
+ dt.time.min as int == 12 &&
+ dt.time.sec as int == 50, "invalid parsing results");
+
+ strptime("%D", "08/2/1994", &dt)!;
+ assert(dt.date.day as int == 2 &&
+ dt.date.month as int == 8 &&
+ dt.date.year as int == 1994, "invalid parsing results");
+
+ strptime("%F", "1994-08-27", &dt)!;
+ assert(dt.date.day as int == 27 &&
+ dt.date.month as int == 08 &&
+ dt.date.year as int == 1994, "invalid parsing results");
+
+ strptime("%r", "04:20:12 PM", &dt)!;
+ assert(dt.time.hour as int == 16 &&
+ dt.time.min as int == 20 &&
+ dt.time.sec as int == 12, "invalid parsing results");
+
+ strptime("%r", "04:20:12 AM", &dt)!;
+ assert(dt.time.hour as int == 04 &&
+ dt.time.min as int == 20 &&
+ dt.time.sec as int == 12, "invalid parsing results");
+
+ strptime("%R", "12:2", &dt)!;
+ assert(dt.time.hour as int == 12 &&
+ dt.time.min as int == 2, "invalid parsing results");
+
+ strptime("%T", "12:2:12", &dt)!;
+ assert(dt.time.hour as int == 12 &&
+ dt.time.min as int == 2 &&
+ dt.time.sec as int == 12, "invalid parsing results");
+};
diff --git a/datetime/format.ha b/datetime/format.ha
@@ -1,3 +1,4 @@
+use ascii;
use errors;
use fmt;
use io;
@@ -19,7 +20,7 @@ def WEEKDAYS_SHORT: [_]str = ["Mon", "Tue", "Wed", "Thu", "Fr", "Sat", "Sun"];
def MONTHS: [_]str = [
"January",
- "Feburary",
+ "February",
"March",
"April",
"May",
@@ -39,9 +40,388 @@ def MONTHS_SHORT: [_]str = [
"Oct", "Nov", "Dec",
];
+fn get_default_locale_string_index(iter: *strings::iterator, list: []str) (int | errors::invalid) = {
+ const name = strings::iter_str(iter);
+ if (len(name) == 0) {
+ return errors::invalid;
+ };
+ for(let i = 0z; i < len(list); i += 1) {
+ if (strings::hasprefix(name, list[i])) {
+ // Consume name
+ for (let j = 0z; j < len(list[i]); j += 1) {
+ strings::next(iter);
+ };
+ return (i: int) + 1;
+ };
+ };
+ return errors::invalid;
+};
+
+fn get_max_n_digits(iter: *strings::iterator, n: uint) (int | errors::invalid) = {
+ let buf: [64]u8 = [0...];
+ let bufstr = strio::fixed(buf);
+ defer io::close(bufstr);
+ for (let i = 0z; i < n; i += 1) {
+ let r: rune = match (strings::next(iter)) {
+ case void =>
+ break;
+ case r: rune =>
+ yield r;
+ };
+ if (!ascii::isdigit(r)) {
+ strings::prev(iter);
+ break;
+ };
+ match (strio::appendrune(bufstr, r)) {
+ case io::error =>
+ return errors::invalid;
+ case =>
+ void;
+ };
+ };
+ return match (strconv::stoi(strio::string(bufstr))) {
+ case res: int =>
+ yield res;
+ case =>
+ yield errors::invalid;
+ };
+};
+
+fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | errors::invalid) = {
+ let s_r = match (strings::next(iter)) {
+ case void =>
+ return errors::invalid;
+ case r: rune =>
+ yield r;
+ };
+ if (s_r == needle) {
+ return 1;
+ } else {
+ strings::prev(iter);
+ return 0;
+ };
+};
+
+fn clamp_int(i: int, min: int, max: int) int = {
+ return if (i < min) {
+ yield min;
+ } else if (i > max) {
+ yield max;
+ } else {
+ yield i;
+ };
+};
+
// Parses a string into a [[datetime]]
export fn strptime(format: str, s: str, dt: *datetime) (void | errors::invalid) = {
- // TODO
+ const format_iter = strings::iter(format);
+ const s_iter = strings::iter(s);
+ let escaped = false;
+ for (true) {
+ let format_r: rune = match (strings::next(&format_iter)) {
+ case void =>
+ break;
+ case r: rune =>
+ yield r;
+ };
+
+ if (!escaped && format_r == '%') {
+ escaped = true;
+ continue;
+ };
+
+ if (!escaped) {
+ let s_r = match (strings::next(&s_iter)) {
+ case void =>
+ return errors::invalid;
+ case r: rune =>
+ yield r;
+ };
+ if (s_r != format_r) {
+ return errors::invalid;
+ };
+ continue;
+ };
+
+ escaped = false;
+ switch (format_r) {
+ // Basic specifiers
+ case 'a' =>
+ // TODO: Localization
+ dt.date.weekday = get_default_locale_string_index(
+ &s_iter, WEEKDAYS_SHORT[..])?;
+ case 'A' =>
+ // TODO: Localization
+ dt.date.weekday = get_default_locale_string_index(
+ &s_iter, WEEKDAYS[..])?;
+ case 'b', 'h' =>
+ // TODO: Localization
+ dt.date.month = get_default_locale_string_index(
+ &s_iter, MONTHS_SHORT[..])?;
+ case 'B' =>
+ // TODO: Localization
+ dt.date.month = get_default_locale_string_index(
+ &s_iter, MONTHS[..])?;
+ case 'd', 'e' =>
+ let max_n_digits = 2u;
+ if (format_r == 'e') {
+ max_n_digits -= eat_one_rune(&s_iter, ' ')?;
+ };
+ dt.date.day = clamp_int(
+ get_max_n_digits(&s_iter, max_n_digits)?, 1, 31);
+ case 'G' =>
+ dt.date.isoweekyear = get_max_n_digits(&s_iter, 4)?;
+ case 'H', 'k' =>
+ let max_n_digits = 2u;
+ if (format_r == 'k') {
+ max_n_digits -= eat_one_rune(&s_iter, ' ')?;
+ };
+ dt.time.hour = clamp_int(
+ get_max_n_digits(&s_iter, max_n_digits)?, 0, 23);
+ case 'I', 'l' =>
+ let max_n_digits = 2u;
+ if (format_r == 'l') {
+ max_n_digits -= eat_one_rune(&s_iter, ' ')?;
+ };
+ const hour = get_max_n_digits(&s_iter, max_n_digits);
+ dt.time.hour = match (hour) {
+ case hour: int =>
+ yield if (hour > 12) {
+ yield clamp_int(hour - 12, 1, 12);
+ } else {
+ yield clamp_int(hour, 1, 12);
+ };
+ case =>
+ return errors::invalid;
+ };
+ case 'j' =>
+ dt.date.yearday = clamp_int(
+ get_max_n_digits(&s_iter, 3)?, 1, 366);
+ case 'm' =>
+ dt.date.month = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 1, 12);
+ case 'M' =>
+ dt.time.min = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 59);
+ case 'n' =>
+ eat_one_rune(&s_iter, '\n')?;
+ case 'N' =>
+ dt.time.nsec = clamp_int(
+ get_max_n_digits(&s_iter, 3)?, 0, 999);
+ case 'p', 'P' =>
+ // TODO: Localization
+ if (dt.time.hour is void) {
+ // We can't change the hour's am/pm because we
+ // have no hour.
+ return errors::invalid;
+ };
+ let rest = strings::iter_str(&s_iter);
+ let prefix_am = if (format_r == 'p') {
+ yield "AM";
+ } else {
+ yield "am";
+ };
+ let prefix_pm = if (format_r == 'p') {
+ yield "PM";
+ } else {
+ yield "pm";
+ };
+ if (strings::hasprefix(rest, prefix_am)) {
+ if (dt.time.hour as int > 12) {
+ // 13 AM?
+ return errors::invalid;
+ } else if (dt.time.hour as int == 12) {
+ dt.time.hour = 0;
+ };
+ } else if (strings::hasprefix(rest, prefix_pm)) {
+ if (dt.time.hour as int > 12) {
+ // 13 PM?
+ return errors::invalid;
+ } else if (dt.time.hour as int < 12) {
+ dt.time.hour =
+ (dt.time.hour as int) + 12;
+ };
+ } else {
+ return errors::invalid;
+ };
+ strings::next(&s_iter);
+ strings::next(&s_iter);
+ case 'S' =>
+ dt.time.sec = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 61);
+ case 't' =>
+ eat_one_rune(&s_iter, '\t')?;
+ case 'u', 'w' =>
+ dt.date.weekday = match (get_max_n_digits(&s_iter, 1)) {
+ case i: int =>
+ yield if (format_r == 'w') {
+ yield if (i == 0) {
+ yield 7;
+ } else {
+ yield clamp_int(i, 1, 7);
+ };
+ } else {
+ yield clamp_int(i, 1, 7);
+ };
+ case =>
+ return errors::invalid;
+ };
+ case 'U', 'W' =>
+ dt.date.week = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 53);
+ case 'V' =>
+ dt.date.isoweek = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 53);
+ case 'Y' =>
+ dt.date.year = get_max_n_digits(&s_iter, 4)?;
+ case 'z' =>
+ // TODO
+ continue;
+ case '%' =>
+ eat_one_rune(&s_iter, '%')?;
+
+ // Expansion specifiers
+ case 'c' =>
+ // TODO: Localization
+ dt.date.weekday = get_default_locale_string_index(
+ &s_iter, WEEKDAYS_SHORT[..])?;
+ if (eat_one_rune(&s_iter, ' ')? != 1) {
+ fmt::printfln("no space after weekday")!;
+ return errors::invalid;
+ };
+ dt.date.month = get_default_locale_string_index(
+ &s_iter, MONTHS_SHORT[..])?;
+ if (eat_one_rune(&s_iter, ' ')? != 1) {
+ fmt::printfln("no space after month")!;
+ return errors::invalid;
+ };
+ const max_n_digits = 2 - eat_one_rune(&s_iter, ' ')?;
+ dt.date.day = clamp_int(
+ get_max_n_digits(&s_iter, max_n_digits)?, 1, 31);
+ if (eat_one_rune(&s_iter, ' ')? != 1) {
+ fmt::printfln("no space after day")!;
+ return errors::invalid;
+ };
+ dt.time.hour = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 23);
+ if (eat_one_rune(&s_iter, ':')? != 1) {
+ fmt::printfln("no : after hour")!;
+ return errors::invalid;
+ };
+ dt.time.min = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 59);
+ if (eat_one_rune(&s_iter, ':')? != 1) {
+ fmt::printfln("no : after minute")!;
+ return errors::invalid;
+ };
+ dt.time.sec = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 61);
+ if (eat_one_rune(&s_iter, ' ')? != 1) {
+ fmt::printfln("no space after sec")!;
+ return errors::invalid;
+ };
+ dt.date.year = get_max_n_digits(&s_iter, 4)?;
+ case 'D', 'x' =>
+ // TODO: Localization for %x
+ dt.date.month = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 1, 12);
+ if (eat_one_rune(&s_iter, '/')? != 1) {
+ return errors::invalid;
+ };
+ dt.date.day = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 1, 31);
+ if (eat_one_rune(&s_iter, '/')? != 1) {
+ return errors::invalid;
+ };
+ dt.date.year = get_max_n_digits(&s_iter, 4)?;
+ case 'F' =>
+ dt.date.year = get_max_n_digits(&s_iter, 4)?;
+ if (eat_one_rune(&s_iter, '-')? != 1) {
+ return errors::invalid;
+ };
+ dt.date.month = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 1, 12);
+ if (eat_one_rune(&s_iter, '-')? != 1) {
+ return errors::invalid;
+ };
+ dt.date.day = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 1, 31);
+ case 'r' =>
+ // TODO: Localization
+ // Time
+ dt.time.hour = match (get_max_n_digits(&s_iter, 2)) {
+ case hour: int =>
+ yield if (hour > 12) {
+ yield clamp_int(hour - 12, 1, 12);
+ } else {
+ yield clamp_int(hour, 1, 12);
+ };
+ case =>
+ return errors::invalid;
+ };
+ if (eat_one_rune(&s_iter, ':')? != 1) {
+ return errors::invalid;
+ };
+ dt.time.min = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 59);
+ if (eat_one_rune(&s_iter, ':')? != 1) {
+ return errors::invalid;
+ };
+ dt.time.sec = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 61);
+ if (eat_one_rune(&s_iter, ' ')? != 1) {
+ return errors::invalid;
+ };
+ let rest = strings::iter_str(&s_iter);
+ // AM/PM
+ if (strings::hasprefix(rest, "AM")) {
+ if (dt.time.hour as int > 12) {
+ // 13 AM?
+ return errors::invalid;
+ } else if (dt.time.hour as int == 12) {
+ dt.time.hour = 0;
+ };
+ } else if (strings::hasprefix(rest, "PM")) {
+ if (dt.time.hour as int > 12) {
+ // 13 PM?
+ return errors::invalid;
+ } else if (dt.time.hour as int < 12) {
+ dt.time.hour =
+ (dt.time.hour as int) + 12;
+ };
+ } else {
+ return errors::invalid;
+ };
+ strings::next(&s_iter);
+ strings::next(&s_iter);
+ case 'R' =>
+ dt.time.hour = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 23);
+ if (eat_one_rune(&s_iter, ':')? != 1) {
+ return errors::invalid;
+ };
+ dt.time.min = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 59);
+ case 'T', 'X' =>
+ // TODO: Localization for %X
+ dt.time.hour = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 23);
+ if (eat_one_rune(&s_iter, ':')? != 1) {
+ return errors::invalid;
+ };
+ dt.time.min = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 59);
+ if (eat_one_rune(&s_iter, ':')? != 1) {
+ return errors::invalid;
+ };
+ dt.time.sec = clamp_int(
+ get_max_n_digits(&s_iter, 2)?, 0, 61);
+
+ case =>
+ // Ignore invalid specifier
+ continue;
+ };
+ };
return void;
};
@@ -67,7 +447,7 @@ export fn strftime(format: str, dt: *datetime) (str | errors::invalid | io::erro
// Formats a [[datetime]] and writes it into a [[io::handle]].
// Fails a particular field is required but void.
export fn fmttime(h: io::handle, format: str, dt: *datetime) (size | errors::invalid | io::error) = {
- let iter = strings::iter(format);
+ const iter = strings::iter(format);
let escaped = false;
let n = 0z;
for (true) {
@@ -91,14 +471,16 @@ export fn fmttime(h: io::handle, format: str, dt: *datetime) (size | errors::inv
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' =>
+ case 'b', 'h' =>
+ // TODO: Localization
yield MONTHS_SHORT[month(dt) - 1];
- case 'h' =>
- yield strftime("%b", dt)?;
case 'B' =>
+ // TODO: Localization
yield MONTHS[month(dt) - 1];
case 'c' =>
// TODO: Localization
diff --git a/stdlib.mk b/stdlib.mk
@@ -2893,12 +2893,13 @@ $(TESTCACHE)/crypto/curve25519/crypto_curve25519-any.ssa: $(testlib_crypto_curve
# datetime (+any)
testlib_datetime_any_srcs= \
$(STDLIB)/datetime/calendar.ha \
- $(STDLIB)/datetime/datetime.ha \
- $(STDLIB)/datetime/timezone.ha \
- $(STDLIB)/datetime/date.ha \
$(STDLIB)/datetime/date+test.ha \
+ $(STDLIB)/datetime/date.ha \
+ $(STDLIB)/datetime/datetime.ha \
+ $(STDLIB)/datetime/format+test.ha \
+ $(STDLIB)/datetime/format.ha \
$(STDLIB)/datetime/time.ha \
- $(STDLIB)/datetime/format.ha
+ $(STDLIB)/datetime/timezone.ha
$(TESTCACHE)/datetime/datetime-any.ssa: $(testlib_datetime_any_srcs) $(testlib_rt) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM))
@printf 'HAREC \t$@\n'