From 3e676e6e45e3c85aa64d567c620f353864aa7a03 Mon Sep 17 00:00:00 2001 From: shalei Date: Wed, 14 Feb 2024 04:38:28 +0800 Subject: [PATCH 1/3] feat: supoort format --- README.md | 6 ++ morrow/constants.mojo | 47 +++++++++++ morrow/formatter.mojo | 185 ++++++++++++++++++++++++++++++++++++++++++ morrow/morrow.mojo | 26 ++++++ morrow/timezone.mojo | 6 +- test.mojo | 12 +++ 6 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 morrow/formatter.mojo diff --git a/README.md b/README.md index ac38699..5d87473 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,12 @@ print(str(utc_t)) # 2023-09-30T16:00:00.000000+00:00 let m = Morrow(2023, 10, 1, 0, 0, 0, 1234) print(m.isoformat()) # 2023-10-01T00:00:00.001234 +# custom format +let m = Morrow(2023, 10, 1, 0, 0, 0, 1234) +print(m.format("YYYY-MM-DD HH:mm:ss.SSSSSS ZZ")) # 2023-10-01 00:00:00.001234 +00:00 +print(m.format("dddd, DD MMM YYYY HH:mm:ss ZZZ")) # Sunday, 01 Oct 2023 00:00:00 UTC +print(m.format("YYYY[Y]MM[M]DD[D]")) # 2023Y10M01D + # Get ISO format with time zone. let m_beijing = Morrow(2023, 10, 1, 0, 0, 0, 1234, TimeZone(28800, 'Bejing')) print(m_beijing.isoformat(timespec="seconds")) # 2023-10-01T00:00:00+08:00 diff --git a/morrow/constants.mojo b/morrow/constants.mojo index ccfb517..3ace9e3 100644 --- a/morrow/constants.mojo +++ b/morrow/constants.mojo @@ -10,3 +10,50 @@ alias _DAYS_IN_MONTH = VariadicList[Int]( alias _DAYS_BEFORE_MONTH = VariadicList[Int]( -1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ) # -1 is a placeholder for indexing purposes. + + +alias MONTH_NAMES = StaticTuple[13]( + "", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +) + +alias MONTH_ABBREVIATIONS = StaticTuple[13]( + "", + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +) + +alias DAY_NAMES = StaticTuple[8]( + "", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +) +alias DAY_ABBREVIATIONS = StaticTuple[8]( + "", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" +) diff --git a/morrow/formatter.mojo b/morrow/formatter.mojo new file mode 100644 index 0000000..eaad958 --- /dev/null +++ b/morrow/formatter.mojo @@ -0,0 +1,185 @@ +from collections.vector import InlinedFixedVector +from utils.static_tuple import StaticTuple +from .util import rjust +from .constants import MONTH_NAMES, MONTH_ABBREVIATIONS, DAY_NAMES, DAY_ABBREVIATIONS +from .timezone import UTC_TZ + +alias formatter = _Formatter() + + +struct _Formatter: + var _sub_chrs: InlinedFixedVector[Int, 128] + + fn __init__(inout self): + self._sub_chrs = InlinedFixedVector[Int, 128](0) + for i in range(128): + self._sub_chrs[i] = 0 + self._sub_chrs[_Y] = 4 + self._sub_chrs[_M] = 4 + self._sub_chrs[_D] = 2 + self._sub_chrs[_d] = 4 + self._sub_chrs[_H] = 2 + self._sub_chrs[_h] = 2 + self._sub_chrs[_m] = 2 + self._sub_chrs[_s] = 2 + self._sub_chrs[_S] = 6 + self._sub_chrs[_Z] = 3 + self._sub_chrs[_A] = 1 + self._sub_chrs[_a] = 1 + + fn format(self, m: Morrow, fmt: String) raises -> String: + """ + "YYYY[abc]MM" -> repalce("YYYY") + "abc" + replace("MM") + """ + if len(fmt) == 0: + return "" + var ret: String = "" + var in_bracket = False + var start_idx = 0 + for i in range(len(fmt)): + if fmt[i] == "[": + if in_bracket: + ret += "[" + else: + in_bracket = True + ret += self.replace(m, fmt[start_idx:i]) + start_idx = i + 1 + elif fmt[i] == "]": + if in_bracket: + ret += fmt[start_idx:i] + in_bracket = False + else: + ret += self.replace(m, fmt[start_idx:i]) + ret += "]" + start_idx = i + 1 + if in_bracket: + ret += "[" + if start_idx < len(fmt): + ret += self.replace(m, fmt[start_idx:]) + return ret + + fn replace(self, m: Morrow, s: String) raises -> String: + """ + split token and replace + """ + if len(s) == 0: + return "" + var ret: String = "" + var match_chr_ord = 0 + var match_count = 0 + for i in range(len(s)): + let c = ord(s[i]) + if 0 < c < 128 and self._sub_chrs[c] > 0: + if c == match_chr_ord: + match_count += 1 + else: + ret += self.replace_token(m, match_chr_ord, match_count) + match_chr_ord = c + match_count = 1 + if match_count == self._sub_chrs[c]: + ret += self.replace_token(m, match_chr_ord, match_count) + match_chr_ord = 0 + else: + if match_chr_ord > 0: + ret += self.replace_token(m, match_chr_ord, match_count) + match_chr_ord = 0 + ret += s[i] + if match_chr_ord > 0: + ret += self.replace_token(m, match_chr_ord, match_count) + return ret + + fn replace_token(self, m: Morrow, token: String, token_count: Int) raises -> String: + if token == _Y: + if token_count == 1: + return "Y" + if token_count == 2: + return rjust(m.year, 4, "0")[2:4] + if token_count == 4: + return rjust(m.year, 4, "0") + elif token == _M: + if token_count == 1: + return String(m.month) + if token_count == 2: + return rjust(m.month, 2, "0") + if token_count == 3: + return String(MONTH_ABBREVIATIONS[m.month]) + if token_count == 4: + return String(MONTH_NAMES[m.month]) + elif token == _D: + if token_count == 1: + return String(m.day) + if token_count == 2: + return rjust(m.day, 2, "0") + elif token == _H: + if token_count == 1: + return String(m.hour) + if token_count == 2: + return rjust(m.hour, 2, "0") + elif token == _h: + var h_12 = m.hour + if m.hour > 12: + h_12 -= 12 + if token_count == 1: + return String(h_12) + if token_count == 2: + return rjust(h_12, 2, "0") + elif token == _m: + if token_count == 1: + return String(m.minute) + if token_count == 2: + return rjust(m.minute, 2, "0") + elif token == _s: + if token_count == 1: + return String(m.second) + if token_count == 2: + return rjust(m.second, 2, "0") + elif token == _S: + if token_count == 1: + return String(m.microsecond // 100000) + if token_count == 2: + return rjust(m.microsecond // 10000, 2, "0") + if token_count == 3: + return rjust(m.microsecond // 1000, 3, "0") + if token_count == 4: + return rjust(m.microsecond // 100, 4, "0") + if token_count == 5: + return rjust(m.microsecond // 10, 5, "0") + if token_count == 6: + return rjust(m.microsecond, 6, "0") + elif token == _d: + if token_count == 1: + return String(m.isoweekday()) + if token_count == 3: + return String(DAY_ABBREVIATIONS[m.isoweekday()]) + if token_count == 4: + return String(DAY_NAMES[m.isoweekday()]) + elif token == _Z: + if token_count == 3: + return UTC_TZ.name if m.tz.is_none() else m.tz.name + var separator = "" if token_count == 1 else ":" + if m.tz.is_none(): + return UTC_TZ.format(separator) + else: + return m.tz.format(separator) + + elif token == _a: + return "am" if m.hour < 12 else "pm" + elif token == _A: + return "AM" if m.hour < 12 else "PM" + return "" + + +alias _Y = ord("Y") +alias _M = ord("M") +alias _D = ord("D") +alias _d = ord("d") +alias _H = ord("H") +alias _h = ord("h") +alias _m = ord("m") +alias _s = ord("s") +alias _S = ord("S") +alias _X = ord("X") +alias _x = ord("x") +alias _Z = ord("Z") +alias _A = ord("A") +alias _a = ord("a") diff --git a/morrow/morrow.mojo b/morrow/morrow.mojo index 0fdc666..84d463e 100644 --- a/morrow/morrow.mojo +++ b/morrow/morrow.mojo @@ -4,6 +4,7 @@ from ._libc import c_gettimeofday, c_localtime, c_gmtime, c_strptime from ._libc import CTimeval, CTm from .timezone import TimeZone from .timedelta import TimeDelta +from .formatter import formatter from .constants import _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH from python.object import PythonObject from python import Python @@ -129,6 +130,26 @@ struct Morrow(StringableRaising): let tzinfo = TimeZone.from_utc(tz_str) return Self.strptime(date_str, fmt, tzinfo) + fn format(self, fmt: String = "YYYY-MM-DD HH:mm:ss ZZ") raises -> String: + """Returns a string representation of the `Morrow` + formatted according to the provided format string. + + :param fmt: the format string. + + Usage:: + >>> let m = Morrow.now() + >>> m.format('YYYY-MM-DD HH:mm:ss ZZ') + '2013-05-09 03:56:47 -00:00' + + >>> m.format('MMMM DD, YYYY') + 'May 09, 2013' + + >>> m.format() + '2013-05-09 03:56:47 -00:00' + + """ + return formatter.format(self, fmt) + fn isoformat( self, sep: String = "T", timespec: StringLiteral = "auto" ) raises -> String: @@ -276,6 +297,11 @@ struct Morrow(StringableRaising): # start of that month: we're done! return Self(year, month, n + 1) + fn isoweekday(self) raises -> Int: + # "Return day of the week, where Monday == 1 ... Sunday == 7." + # 1-Jan-0001 is a Monday + return self.toordinal() % 7 or 7 + fn __str__(self) raises -> String: return self.isoformat() diff --git a/morrow/timezone.mojo b/morrow/timezone.mojo index cb440b6..15c5963 100644 --- a/morrow/timezone.mojo +++ b/morrow/timezone.mojo @@ -1,6 +1,8 @@ from .util import rjust from ._libc import c_localtime +alias UTC_TZ = TimeZone(0, "UTC") + @value struct TimeZone(Stringable): @@ -60,7 +62,7 @@ struct TimeZone(Stringable): let offset: Int = sign * (hours * 3600 + minutes * 60) return TimeZone(offset) - fn format(self) -> String: + fn format(self, sep: String = ":") -> String: let sign: String let offset_abs: Int if self.offset < 0: @@ -71,4 +73,4 @@ struct TimeZone(Stringable): offset_abs = self.offset let hh = offset_abs // 3600 let mm = offset_abs % 3600 - return sign + rjust(hh, 2, "0") + ":" + rjust(mm, 2, "0") + return sign + rjust(hh, 2, "0") + sep + rjust(mm, 2, "0") diff --git a/test.mojo b/test.mojo index 0f93cbe..baba4df 100644 --- a/test.mojo +++ b/test.mojo @@ -157,6 +157,17 @@ def test_from_to_py(): assert_datetime_equal(m2, dt) +def test_format(): + print("Running test_format") + let m = Morrow(2024, 2, 1, 3, 4, 5, 123456) + assert_equal(m.format("YYYY-MM-DD HH:mm:ss.SSS ZZ"), "2024-02-01 03:04:05.123 +00:00") + assert_equal(m.format("Y-YY-YYY-YYYY M-MM D-DD"), "Y-24--2024 2-02 1-01") + assert_equal(m.format("H-HH-h-hh m-mm s-ss"), "3-03-3-03 4-04 5-05") + assert_equal(m.format("S-SS-SSS-SSSS-SSSSS-SSSSSS"), "1-12-123-1234-12345-123456") + assert_equal(m.format("d-dd-ddd-dddd"), "4--Thu-Thursday") + assert_equal(m.format("YYYY[Y] [[]MM[]][M]"), "2024Y [02]M") + + def main(): test_now() test_utcnow() @@ -168,3 +179,4 @@ def main(): test_strptime() test_timedelta() test_from_to_py() + test_format() From 3d5fcb8135b586accd8ae7e1262acc68ccb85f19 Mon Sep 17 00:00:00 2001 From: Prodesire Date: Sat, 17 Feb 2024 20:02:20 +0800 Subject: [PATCH 2/3] fix: fixed an issue where test cases may fail due to different machine time zone. --- test.mojo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.mojo b/test.mojo index baba4df..f9ad4b7 100644 --- a/test.mojo +++ b/test.mojo @@ -69,8 +69,8 @@ def test_time_zone(): def test_strptime(): print("Running test_strptime()") - m = Morrow.strptime("20-01-2023 15:49:10", "%d-%m-%Y %H:%M:%S", TimeZone.local()) - assert_equal(str(m), "2023-01-20T15:49:10.000000+08:00") + m = Morrow.strptime("20-01-2023 15:49:10", "%d-%m-%Y %H:%M:%S", TimeZone.none()) + assert_equal(str(m), "2023-01-20T15:49:10.000000+00:00") m = Morrow.strptime("2023-10-18 15:49:10 +0800", "%Y-%m-%d %H:%M:%S %z") assert_equal(str(m), "2023-10-18T15:49:10.000000+08:00") From efb0ad7cc3097905494bbfdb822425a2b770df41 Mon Sep 17 00:00:00 2001 From: Prodesire Date: Sat, 17 Feb 2024 20:06:37 +0800 Subject: [PATCH 3/3] feat: update test and version --- morrow/__init__.mojo | 2 +- test.mojo | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/morrow/__init__.mojo b/morrow/__init__.mojo index bf2bf2e..3ab86e9 100644 --- a/morrow/__init__.mojo +++ b/morrow/__init__.mojo @@ -2,4 +2,4 @@ from .morrow import Morrow from .timezone import TimeZone from .timedelta import TimeDelta -alias __version__ = "0.2.0" +alias __version__ = "0.3.0" diff --git a/test.mojo b/test.mojo index f9ad4b7..b81224a 100644 --- a/test.mojo +++ b/test.mojo @@ -148,7 +148,7 @@ def test_timedelta(): def test_from_to_py(): - print("Running test_from_to_py") + print("Running test_from_to_py()") m = Morrow.now() dt = m.to_py() assert_datetime_equal(m, dt) @@ -158,7 +158,7 @@ def test_from_to_py(): def test_format(): - print("Running test_format") + print("Running test_format()") let m = Morrow(2024, 2, 1, 3, 4, 5, 123456) assert_equal(m.format("YYYY-MM-DD HH:mm:ss.SSS ZZ"), "2024-02-01 03:04:05.123 +00:00") assert_equal(m.format("Y-YY-YYY-YYYY M-MM D-DD"), "Y-24--2024 2-02 1-01")