From 1933d88dd5865d8bd4d6ed41b15bc50ea0ecb753 Mon Sep 17 00:00:00 2001 From: litlighilit Date: Fri, 12 Jul 2024 16:10:14 +0800 Subject: [PATCH] func: datetime.str{f,p}time --- .../Lib/datetime_impl/datetime_impl/meth.nim | 6 +- .../meth/require_time_module.nim | 98 +++++++++++++++++++ .../Lib/datetime_impl/timezone_impl/decl.nim | 77 +++++++++------ src/pylib/Lib/test/test_datetime.nim | 17 ++++ 4 files changed, 165 insertions(+), 33 deletions(-) create mode 100644 src/pylib/Lib/datetime_impl/datetime_impl/meth/require_time_module.nim diff --git a/src/pylib/Lib/datetime_impl/datetime_impl/meth.nim b/src/pylib/Lib/datetime_impl/datetime_impl/meth.nim index d9d47e4db..6b8d9c8af 100644 --- a/src/pylib/Lib/datetime_impl/datetime_impl/meth.nim +++ b/src/pylib/Lib/datetime_impl/datetime_impl/meth.nim @@ -1,6 +1,8 @@ import ./meth/[ - consts, statics, init, getter, getter_requires_op, getter_of_date, hashImpl, op, aszone + consts, statics, init, getter, getter_requires_op, getter_of_date, + hashImpl, op, aszone, require_time_module ] -export consts, statics, init, getter, getter_requires_op, getter_of_date, hashImpl, op, aszone +export consts, statics, init, getter, getter_requires_op, getter_of_date, + hashImpl, op, aszone, require_time_module diff --git a/src/pylib/Lib/datetime_impl/datetime_impl/meth/require_time_module.nim b/src/pylib/Lib/datetime_impl/datetime_impl/meth/require_time_module.nim new file mode 100644 index 000000000..05cd78547 --- /dev/null +++ b/src/pylib/Lib/datetime_impl/datetime_impl/meth/require_time_module.nim @@ -0,0 +1,98 @@ + +import ../../../time_impl/[ + types, converters, nstrfptime +] + +from std/strutils import multiReplace, replace, align, parseInt +import ../../timedelta_impl/decl +import ../../timezone_impl/[ + decl, meth_by_datetime_getter +] + +include ./common +from std/times import DateTime, yearday, toParts +import ./getter_of_date + +from ./importer import NotImplementedError + +using self: datetime + +# shall also has a `self: date` +func timetuple*(self): struct_time = + var dstflag = -1 + if not self.tzinfo.isTzNone: + let dst = self.tzinfo.dst(self) + if not dst.isTimeDeltaNone: + dstflag = int bool dst + self.asNimDatetime.dtToStructTime result + +using tzinfoarg: datetime + +proc add_somezreplacement(s: var string, sep: string, tzinfo: tzinfo, tzinfoarg) = + var offset: timedelta + try: + offset = tzinfo.utcoffset(tzinfoarg) + except ValueError, NotImplementedError: + return + s.add format_utcoffset(offset.asDuration.toParts(), sep, prefix="") + +const Py_NORMALIZE_CENTURY = true # since CPython gh-120713 +proc add_Zreplacement(s: var string, tzinfo: tzinfo, tzinfoarg) = + try: + s.add tzinfo.tzname(tzinfoarg) + except NotImplementedError: + return + #[Since the tzname is getting stuffed into the + format, we have to double any % signs so that + strftime doesn't treat them as format codes.]# + s = s.replace("%", "%%") + +func wrap_strftime(self; format: string, tzinfoarg): string = + #[Scan the input format, looking for %z/%Z/%f escapes, building + a new format. Since computing the replacements for those codes + is expensive, don't unless they're actually used.]# + + let tzinfo = self.tzinfo + var z_repl, Z_repl, z_col_repl: string + if not tzinfo.isTzNone: + z_repl.add_somezreplacement("", tzinfo, self) + Z_repl.add_Zreplacement(tzinfo, self) + z_col_repl.add_somezreplacement(":", tzinfo, self) + + let baseRepl = { + "%z": z_repl, + "%Z": Z_repl, + "%:z": z_col_repl, + "%f": align($self.microsecond, 6, '0') + } + when Py_NORMALIZE_CENTURY: + let year = self.year + template pad4(i: SomeInteger): string = + align($i, 4, '0') + let + iG = parseInt(strftime("%G", self.asNimDatetime)) + iY = self.year + let newfmt = format.multiReplace( + @baseRepl & @{ + "%G": iG.pad4, + "%Y": iY.pad4, + } + ) + else: + let newfmt = format.multiReplace(baseRepl) + + strftime(newfmt, self.asNimDatetime) + +func strftime*(self; format: string): string = + wrap_strftime(self, format, self) + +using _: typedesc[datetime] +func strptime*(_; datetime_string, format: string): datetime = + var ndt: DateTime + ndt.strptime(datetime_string, format) + result = newDatetime( + ndt, + ) + +# TODO: easy: support all in nstrfptime.NotImplDirectives, with the help of calendar_utils + diff --git a/src/pylib/Lib/datetime_impl/timezone_impl/decl.nim b/src/pylib/Lib/datetime_impl/timezone_impl/decl.nim index 01e2ef21f..050ea7380 100644 --- a/src/pylib/Lib/datetime_impl/timezone_impl/decl.nim +++ b/src/pylib/Lib/datetime_impl/timezone_impl/decl.nim @@ -82,49 +82,64 @@ func repr*(self: timezone): string = result.add self.name result.add ')' -func `$`*(self: timezone): string = - if self.name.len != 0: return self.name - let parts = self.offset.asDuration.toParts() - let - days = parts[Days] - secs = parts[Seconds] - us = parts[Microseconds] - if self.is_const_utc or - days == 0 and secs == 0 and us == 0: - return "UTC" +func format_utcoffset(hours, minutes, seconds, microseconds: int, + sep: char|string = ':', prefix="UTC"): string = var - sign: char - offset: timedelta - if days < 0: + hours = hours + minutes = minutes + seconds = seconds + microseconds = microseconds + var sign = '+' + if hours < 0 or minutes < 0 or seconds < 0 or microseconds < 0: + template rev(i: var SomeInteger) = + i = -i sign = '-' - offset = -self.offset - else: - sign = '+' - offset = self.offset # new ref - let - nparts = offset.asDuration.toParts() - microseconds = nparts[Microseconds] - var seconds = nparts[Seconds] - var minutes = divmod(seconds, 60, seconds) - let hours = divmod(minutes, 60, minutes) - result = newStringOfCap 12 - result.add "UTC" - template add(c: char) = result.add c - add sign + rev hours + rev minutes + rev seconds + rev microseconds + let sepLen = when sep is char: 1 else: sep.len + result = newStringOfCap 7 + prefix.len + 3 * sepLen + result.add prefix + result.add sign + template add(cs: char|string) = result.add cs template add0Nd(d: SomeInteger, n: int) = - # we know d >= 0 + # we know `d` >= 0 let sd = $d for _ in 1..(n-sd.len): result.add '0' result.add sd template add02d(d: SomeInteger) = add0Nd(d, 2) add02d hours - add ':' + add sep add02d minutes if seconds != 0: - add ':' + add sep add02d seconds if microseconds != 0: - result.setLen 19 + result.setLen result.len + 7 add '.' add0Nd microseconds, 6 + +func format_utcoffset*(parts: DurationParts, sep: string|char = ':', + prefix="UTC"): string = + ## common code of CPython C-API `timezone_str` and `format_utcoffset` + ## in `_datetimemodule.c + when not defined(release): + let days = parts[Days] + parts[Weeks] * 7 + assert days == 0, "timezone's init shall check its offset is within one day" + let + hours = parts[Hours] + minutes = parts[Minutes] + seconds = parts[Seconds] + microseconds = parts[Microseconds] + format_utcoffset(hours, minutes, seconds, microseconds, + sep=sep, prefix=prefix) + +func `$`*(self: timezone): string = + if self.isTzNone: return "None" + if self.name.len != 0: return self.name + if self.is_const_utc or + bool(self.offset) == false: + return "UTC" + format_utcoffset(self.offset.asDuration.toParts(), sep=':', prefix="UTC") diff --git a/src/pylib/Lib/test/test_datetime.nim b/src/pylib/Lib/test/test_datetime.nim index 7e861e05d..9dd36c33b 100644 --- a/src/pylib/Lib/test/test_datetime.nim +++ b/src/pylib/Lib/test/test_datetime.nim @@ -241,3 +241,20 @@ suite "TestDate": base = theclass(2000, 2, 29) expect(ValueError): _ = base.replace(year=2001) + test "strftime": + def test_strftime(): + t = theclass(2005, 3, 2) + assertEqual(t.strftime("m:%m d:%d y:%Y"), "m:03 d:02 y:2005") + assertEqual(t.strftime(""), "") # SF bug #761337 + assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 + + check not compiles(t.strftime) # needs an arg + check not compiles(t.strftime("one", "two")) # too many args + check not compiles(t.strftime(42)) # arg wrong type + + # test that unicode input is allowed (issue 2782) + assertEqual(t.strftime("%m"), "03") + + # A naive object replaces %z, %:z and %Z w/ empty strings. + assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''") + test_strftime()