From f0acee4844c7b6bd761d8788d5d0e2a6ce7de270 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Tue, 7 Jul 2026 14:38:28 +0200 Subject: [PATCH 1/2] Fix %I/%-I strftime directives to use the 12-hour clock The %I and %-I directives mapped straight to the hour attribute (0-23), making them identical to %H. Per the strftime specification (and the '12' fallback already present) they are the 12-hour clock, so e.g. 1pm rendered as '13' instead of '01' and midnight as '00' instead of '12'. Add an hour12 property (hour % 12, with 0 mapped to 12) and point both directives at it. A plain date (no time) still falls back to '12'. --- CHANGELOG.md | 1 + jdatetime/__init__.py | 10 ++++++++-- tests/test_jdatetime.py | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c74e8a6..6ce05a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +* `%I` and `%-I` now render the 12-hour clock instead of duplicating `%H` * pass Gregorian to tzinfo.dst #188 * typing: use int for fold #186 diff --git a/jdatetime/__init__.py b/jdatetime/__init__.py index 59d51a7..606a3ad 100644 --- a/jdatetime/__init__.py +++ b/jdatetime/__init__.py @@ -45,8 +45,8 @@ '%f': ('_strftime_get_attr_value', {'attr': 'microsecond', 'fmt': '%06.d', 'fb': '000000'}), '%H': ('_strftime_get_attr_value', {'attr': 'hour', 'fmt': '%02.d', 'fb': '00'}), '%-H': ('_strftime_get_attr_value', {'attr': 'hour', 'fmt': '%d', 'fb': '0'}), - '%I': ('_strftime_get_attr_value', {'attr': 'hour', 'fmt': '%02.d', 'fb': '12'}), - '%-I': ('_strftime_get_attr_value', {'attr': 'hour', 'fmt': '%d', 'fb': '12'}), + '%I': ('_strftime_get_attr_value', {'attr': 'hour12', 'fmt': '%02.d', 'fb': '12'}), + '%-I': ('_strftime_get_attr_value', {'attr': 'hour12', 'fmt': '%d', 'fb': '12'}), '%M': ('_strftime_get_attr_value', {'attr': 'minute', 'fmt': '%02.d', 'fb': '00'}), '%-M': ('_strftime_get_attr_value', {'attr': 'minute', 'fmt': '%d', 'fb': '0'}), '%S': ('_strftime_get_attr_value', {'attr': 'second', 'fmt': '%02.d', 'fb': '00'}), @@ -871,6 +871,12 @@ def fromordinal(ordinal: int) -> datetime: def hour(self) -> int: return self.__time.hour + @property + def hour12(self) -> int: + """Return the hour on a 12-hour clock (1..12), used by the %I directive.""" + hour = self.hour % 12 + return 12 if hour == 0 else hour + @property def minute(self) -> int: return self.__time.minute diff --git a/tests/test_jdatetime.py b/tests/test_jdatetime.py index e9fddc3..9cdbb0f 100644 --- a/tests/test_jdatetime.py +++ b/tests/test_jdatetime.py @@ -211,6 +211,31 @@ def dst(self, dt): dt = jdatetime.datetime(1389, 2, 17, 19, 10, 2, tzinfo=teh) self.assertEqual(dt.strftime('%Z %z'), 'IRDT +0330') + def test_strftime_I_directive_uses_twelve_hour_clock(self): + import datetime as _std + + # %I and %-I must render the 12-hour clock (1..12), matching stdlib. + for hour in range(24): + with self.subTest(hour=hour): + jd = jdatetime.datetime(1400, 1, 1, hour, 5, 0) + std = _std.datetime(2020, 1, 1, hour, 5, 0) + self.assertEqual(jd.strftime('%I'), std.strftime('%I')) + self.assertEqual(jd.strftime('%-I'), std.strftime('%-I')) + self.assertEqual(jd.hour12, int(std.strftime('%I'))) + # %I must be consistent with the (correct) %p directive: + # reconstructing the 24-hour value from (%p, %I) round-trips. + twelve = int(jd.strftime('%I')) + base = 0 if twelve == 12 else twelve + reconstructed = base + (12 if jd.strftime('%p') == 'PM' else 0) + self.assertEqual(reconstructed, hour) + # Spot-check the values that the previous 24-hour behaviour got wrong. + self.assertEqual(jdatetime.datetime(1400, 1, 1, 0, 0).strftime('%I'), '12') + self.assertEqual(jdatetime.datetime(1400, 1, 1, 13, 0).strftime('%I'), '01') + self.assertEqual(jdatetime.datetime(1400, 1, 1, 23, 0).strftime('%I'), '11') + self.assertEqual(jdatetime.datetime(1400, 1, 1, 13, 0).strftime('%-I'), '1') + # A plain date (no time component) is midnight -> 12. + self.assertEqual(jdatetime.date(1400, 1, 1).strftime('%I'), '12') + def test_strftime_fa_locale_uses_short_month_names_for_b_directive(self): tests = [ (1, 'فرو', 'فروردین'), From 5ca3e1110e3559ed018334acddf9a232b87400bb Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Wed, 15 Jul 2026 09:59:13 +0200 Subject: [PATCH 2/2] Make hour12 property private (_hour12) --- jdatetime/__init__.py | 6 +++--- tests/test_jdatetime.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jdatetime/__init__.py b/jdatetime/__init__.py index 606a3ad..b0df168 100644 --- a/jdatetime/__init__.py +++ b/jdatetime/__init__.py @@ -45,8 +45,8 @@ '%f': ('_strftime_get_attr_value', {'attr': 'microsecond', 'fmt': '%06.d', 'fb': '000000'}), '%H': ('_strftime_get_attr_value', {'attr': 'hour', 'fmt': '%02.d', 'fb': '00'}), '%-H': ('_strftime_get_attr_value', {'attr': 'hour', 'fmt': '%d', 'fb': '0'}), - '%I': ('_strftime_get_attr_value', {'attr': 'hour12', 'fmt': '%02.d', 'fb': '12'}), - '%-I': ('_strftime_get_attr_value', {'attr': 'hour12', 'fmt': '%d', 'fb': '12'}), + '%I': ('_strftime_get_attr_value', {'attr': '_hour12', 'fmt': '%02.d', 'fb': '12'}), + '%-I': ('_strftime_get_attr_value', {'attr': '_hour12', 'fmt': '%d', 'fb': '12'}), '%M': ('_strftime_get_attr_value', {'attr': 'minute', 'fmt': '%02.d', 'fb': '00'}), '%-M': ('_strftime_get_attr_value', {'attr': 'minute', 'fmt': '%d', 'fb': '0'}), '%S': ('_strftime_get_attr_value', {'attr': 'second', 'fmt': '%02.d', 'fb': '00'}), @@ -872,7 +872,7 @@ def hour(self) -> int: return self.__time.hour @property - def hour12(self) -> int: + def _hour12(self) -> int: """Return the hour on a 12-hour clock (1..12), used by the %I directive.""" hour = self.hour % 12 return 12 if hour == 0 else hour diff --git a/tests/test_jdatetime.py b/tests/test_jdatetime.py index 9cdbb0f..5f3402a 100644 --- a/tests/test_jdatetime.py +++ b/tests/test_jdatetime.py @@ -221,7 +221,7 @@ def test_strftime_I_directive_uses_twelve_hour_clock(self): std = _std.datetime(2020, 1, 1, hour, 5, 0) self.assertEqual(jd.strftime('%I'), std.strftime('%I')) self.assertEqual(jd.strftime('%-I'), std.strftime('%-I')) - self.assertEqual(jd.hour12, int(std.strftime('%I'))) + self.assertEqual(jd._hour12, int(std.strftime('%I'))) # %I must be consistent with the (correct) %p directive: # reconstructing the 24-hour value from (%p, %I) round-trips. twelve = int(jd.strftime('%I'))