hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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:
Adatetime/format+test.ha | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdatetime/format.ha | 394+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mstdlib.mk | 9+++++----
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'