Skip to content

Commit 1e17df6

Browse files
author
Sylvain MARIE
committed
Now supporting %y (short year), %I (12h clock hour) and %p (AM/PM). Note that the latter is locale-specific... Fixed Period.strftime %l %m and %n : Fixed pandas-dev#46252
1 parent 2c23f48 commit 1e17df6

File tree

6 files changed

+134
-45
lines changed

6 files changed

+134
-45
lines changed

pandas/_libs/tslib.pyx

+15-5
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def format_array_from_datetime(
128128
np.ndarray[object]
129129
"""
130130
cdef:
131-
int64_t val, ns, N = len(values)
131+
int64_t val, ns, y, h, N = len(values)
132132
ndarray[int64_t] consider_values
133133
bint show_ms = False, show_us = False, show_ns = False
134134
bint basic_format = False
@@ -203,10 +203,20 @@ def format_array_from_datetime(
203203
dt64_to_dtstruct(val, &dts)
204204

205205
# Use string formatting for faster strftime
206-
result[i] = str_format % dict(
207-
year=dts.year, month=dts.month, day=dts.day, hour=dts.hour,
208-
min=dts.min, sec=dts.sec, us=dts.us
209-
)
206+
y = dts.year
207+
h = dts.hour
208+
result[i] = str_format % {
209+
"year": y,
210+
"shortyear": y % 100,
211+
"month": dts.month,
212+
"day": dts.day,
213+
"hour": dts.hour,
214+
"hour12": 12 if h in (0, 12) else (h % 12),
215+
"ampm": "PM" if (h // 12) else "AM",
216+
"min": dts.min,
217+
"sec": dts.sec,
218+
"us": dts.us,
219+
}
210220
else:
211221
ts = Timestamp(val, tz=tz)
212222

pandas/_libs/tslibs/period.pyx

+26-13
Original file line numberDiff line numberDiff line change
@@ -1251,19 +1251,19 @@ cdef str _period_fast_strftime(int64_t value, int freq):
12511251
# fmt = b'%Y-%m-%d %H:%M:%S.%l'
12521252
return (f"{dts.year}-{dts.month:02d}-{dts.day:02d} "
12531253
f"{dts.hour:02d}:{dts.min:02d}:{dts.sec:02d}"
1254-
f".{(value % 1_000):03d}")
1254+
f".{(dts.us // 1_000):03d}")
12551255

12561256
elif freq_group == FR_US:
12571257
# fmt = b'%Y-%m-%d %H:%M:%S.%u'
12581258
return (f"{dts.year}-{dts.month:02d}-{dts.day:02d} "
12591259
f"{dts.hour:02d}:{dts.min:02d}:{dts.sec:02d}"
1260-
f".{(value % 1_000_000):06d}")
1260+
f".{(dts.us):06d}")
12611261

12621262
elif freq_group == FR_NS:
12631263
# fmt = b'%Y-%m-%d %H:%M:%S.%n'
12641264
return (f"{dts.year}-{dts.month:02d}-{dts.day:02d} "
12651265
f"{dts.hour:02d}:{dts.min:02d}:{dts.sec:02d}"
1266-
f".{(value % 1_000_000_000):09d}")
1266+
f".{((dts.us * 1000) + (dts.ps // 1000)):09d}")
12671267

12681268
else:
12691269
raise ValueError(f"Unknown freq: {freq}")
@@ -1323,11 +1323,11 @@ cdef str _period_strftime(int64_t value, int freq, bytes fmt):
13231323
elif i == 2: # %F, 'Fiscal' year with a century
13241324
repl = str(dts.year)
13251325
elif i == 3: # %l, milliseconds
1326-
repl = f"{(value % 1_000):03d}"
1326+
repl = f"{(dts.us // 1_000):03d}"
13271327
elif i == 4: # %u, microseconds
1328-
repl = f"{(value % 1_000_000):06d}"
1328+
repl = f"{(dts.us):06d}"
13291329
elif i == 5: # %n, nanoseconds
1330-
repl = f"{(value % 1_000_000_000):09d}"
1330+
repl = f"{((dts.us * 1000) + (dts.ps // 1000)):09d}"
13311331

13321332
result = result.replace(str_extra_fmts[i], repl)
13331333

@@ -2418,7 +2418,7 @@ cdef class _Period(PeriodMixin):
24182418

24192419
cdef:
24202420
npy_datetimestruct dts, dts2
2421-
int quarter
2421+
int quarter, y, h
24222422

24232423
# Fill dts with all fields
24242424
get_date_info(value, freq, &dts)
@@ -2427,12 +2427,25 @@ cdef class _Period(PeriodMixin):
24272427
quarter = get_yq(value, freq, &dts2)
24282428

24292429
# Finally use the string template
2430-
return fmt_str % dict(
2431-
year=dts.year, month=dts.month, day=dts.day, hour=dts.hour,
2432-
min=dts.min, sec=dts.sec,
2433-
ms=dts.us // 1000, us=dts.us, ns=dts.us * 1000,
2434-
q=quarter, Fyear=dts2.year, fyear=dts2.year % 100
2435-
)
2430+
y = dts.year
2431+
h = dts.hour
2432+
return fmt_str % {
2433+
"year": y,
2434+
"shortyear": y % 100,
2435+
"month": dts.month,
2436+
"day": dts.day,
2437+
"hour": h,
2438+
"hour12": 12 if h in (0, 12) else (h % 12),
2439+
"ampm": "PM" if (h // 12) else "AM",
2440+
"min": dts.min,
2441+
"sec": dts.sec,
2442+
"ms": dts.us // 1000,
2443+
"us": dts.us,
2444+
"ns": (dts.us * 1000) + (dts.ps // 1000),
2445+
"q": quarter,
2446+
"Fyear": dts2.year,
2447+
"fyear": dts2.year % 100,
2448+
}
24362449

24372450
def strftime(self, fmt: str) -> str:
24382451
r"""

pandas/_libs/tslibs/strftime.py

+13-14
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,16 @@ class UnsupportedStrFmtDirective(ValueError):
77

88

99
_COMMON_UNSUPPORTED = (
10-
# All of these below are names and therefore are not in the numpy or datetime attr representation
10+
# 1- Names not in the numpy or datetime attr representation
1111
"%a", # Weekday as locale’s abbreviated name.
1212
"%A", # Weekday as locale’s full name.
1313
"%w", # Weekday as a decimal number, where 0 is Sunday and 6 is Saturday.
1414
"%b", # Month as locale’s abbreviated name.
1515
"%B", # Month as locale’s full name.
16-
# TODO All of those below can probably be derived easily from the numbers on the instance
17-
"%y", # Year without century as a zero-padded decimal number. >> year % 100
18-
"%I", # Hour (12-hour clock) as a zero-padded decimal number. >> hour % 12
19-
"%p", # Locale’s equivalent of either AM or PM. >> "pm" if (hour // 12) else "am"
20-
# TODO Below Time offset and timezone information ... but may be hard
21-
"%z", # UTC offset in the form ±HHMM[SS[.ffffff]] (empty string if the object is naive).
22-
"%Z", # Time zone name (empty string if the object is naive).
23-
# We do not want to enter into these below, we do not want to re-create the datetime implementation
16+
# 2- TODO Below Time offset and timezone information ... but may be hard
17+
"%z", # UTC offset in the form ±HHMM[SS[.ffffff]] ("" if tz naive).
18+
"%Z", # Time zone name ("" if tz naive).
19+
# 3- Probably too complex ones for now
2420
"%j", # Day of the year as a zero-padded decimal number.
2521
"%U", # Week number of the year (Sunday as the first day of the week) as a zero-padded decimal number. All days in a new year preceding the first Sunday are considered to be in week 0.
2622
"%W", # Week number of the year (Monday as the first day of the week) as a zero-padded decimal number. All days in a new year preceding the first Monday are considered to be in week 0.
@@ -34,13 +30,16 @@ class UnsupportedStrFmtDirective(ValueError):
3430
"%d": ("day", "02d"), # Day of the month as a zero-padded decimal number.
3531
"%m": ("month", "02d"), # Month as a zero-padded decimal number.
3632
"%Y": ("year", "d"), # Year with century as a decimal number.
37-
"%H": ("hour", "02d"), # Hour (24-hour clock) as a zero-padded decimal number.
33+
"%y": ("shortyear", "02d"), # Year without century as 0-padded decimal nb.
34+
"%H": ("hour", "02d"), # Hour (24-hour clock) as 0-padded decimal number.
35+
"%I": ("hour12", "02d"), # Hour (12-hour clock) as a 0-padded decimal nb.
36+
"%p": ("ampm", "s"), # Locale’s equivalent of either AM or PM.
3837
"%M": ("min", "02d"), # Minute as a zero-padded decimal number.
3938
"%S": ("sec", "02d"), # Second as a zero-padded decimal number.
4039
}
4140

4241
_DATETIME_MAP = {
43-
"%f": ("us", "06d"), # Microsecond as a decimal number, zero-padded to 6 digits.
42+
"%f": ("us", "06d"), # Microsecond as decimal number, 0-padded to 6 digits
4443
}
4544

4645
_PERIOD_MAP = {
@@ -50,9 +49,9 @@ class UnsupportedStrFmtDirective(ValueError):
5049
), # 'Fiscal' year without century as zero-padded decimal number [00,99]
5150
"%F": ("Fyear", "d"), # 'Fiscal' year with century as a decimal number
5251
"%q": ("q", "d"), # Quarter as a decimal number [1,4]
53-
"%l": ("ms", "03d"), # Microsecond as a decimal number, zero-padded to 3 digits.
54-
"%u": ("us", "06d"), # Microsecond as a decimal number, zero-padded to 6 digits.
55-
"%n": ("ns", "09d"), # Microsecond as a decimal number, zero-padded to 9 digits.
52+
"%l": ("ms", "03d"), # Millisecond as decimal number, 0-padded 3 digits
53+
"%u": ("us", "06d"), # Microsecond as decimal number, 0-padded 6 digits
54+
"%n": ("ns", "09d"), # Nanosecond as decimal number, 0-padded 9 digits
5655
}
5756

5857

pandas/_libs/tslibs/timestamps.pyx

+14-4
Original file line numberDiff line numberDiff line change
@@ -1210,10 +1210,20 @@ class Timestamp(_Timestamp):
12101210
>>> ts.fast_strftime(fmt)
12111211
'2020-03-14T15:32:52'
12121212
"""
1213-
return fmt_str % dict(
1214-
year=self.year, month=self.month, day=self.day, hour=self.hour,
1215-
min=self.minute, sec=self.second, us=self.microsecond
1216-
)
1213+
y = self.year
1214+
h = self.hour
1215+
return fmt_str % {
1216+
"year": y,
1217+
"shortyear": y % 100,
1218+
"month": self.month,
1219+
"day": self.day,
1220+
"hour": h,
1221+
"hour12": 12 if h in (0, 12) else (h % 12),
1222+
"ampm": "PM" if (h // 12) else "AM",
1223+
"min": self.minute,
1224+
"sec": self.second,
1225+
"us": self.microsecond,
1226+
}
12171227

12181228
def strftime(self, format):
12191229
"""

pandas/core/tools/datetimes.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -1315,15 +1315,20 @@ def fast_strftime(
13151315
`datetime.strftime`.
13161316
"""
13171317
# common dict used for formatting
1318-
fmt_dct = dict(
1319-
year=dt.year,
1320-
month=dt.month,
1321-
day=dt.day,
1322-
hour=dt.hour,
1323-
min=dt.minute,
1324-
sec=dt.second,
1325-
us=dt.microsecond,
1326-
)
1318+
y = dt.year
1319+
h = dt.hour
1320+
fmt_dct = {
1321+
"year": y,
1322+
"shortyear": y % 100,
1323+
"month": dt.month,
1324+
"day": dt.day,
1325+
"hour": h,
1326+
"hour12": 12 if h in (0, 12) else (h % 12),
1327+
"ampm": "PM" if (h // 12) else "AM",
1328+
"min": dt.minute,
1329+
"sec": dt.second,
1330+
"us": dt.microsecond,
1331+
}
13271332

13281333
# get the formatting template
13291334
fmt_str = convert_strftime_format(fmt, new_style_fmt=new_style_fmt)

pandas/tests/io/formats/test_format.py

+52
Original file line numberDiff line numberDiff line change
@@ -3169,6 +3169,58 @@ def test_str(self):
31693169
assert str(NaT) == "NaT"
31703170

31713171

3172+
class TestPeriodIndexFormat:
3173+
3174+
def test_period(self):
3175+
p = pd.PeriodIndex([datetime(2003, 1, 1, 12), None], freq='H')
3176+
formatted = p.format()
3177+
assert formatted[0] == "2003-01-01 12:00" # default: minutes not shown
3178+
assert formatted[1] == "NaT"
3179+
3180+
p = pd.period_range("2003-01-01 12:01:01.123456789", periods=2,
3181+
freq="n")
3182+
formatted = p.format()
3183+
assert formatted[0] == '2003-01-01 12:01:01.123456789'
3184+
assert formatted[1] == '2003-01-01 12:01:01.123456790'
3185+
3186+
@pytest.mark.parametrize("fast_strftime", (False, True))
3187+
def test_period_custom(self, fast_strftime):
3188+
# GH46252
3189+
p = pd.period_range("2003-01-01 12:01:01.123", periods=2, freq="l")
3190+
formatted = p.format(date_format="%y %I:%M:%S%p (ms=%l us=%u ns=%n)",
3191+
fast_strftime=fast_strftime)
3192+
assert formatted[0] == "03 12:01:01PM (ms=123 us=123000 ns=123000000)"
3193+
assert formatted[1] == "03 12:01:01PM (ms=124 us=124000 ns=124000000)"
3194+
3195+
p = pd.period_range("2003-01-01 12:01:01.123456789", periods=2, freq="n")
3196+
formatted = p.format(date_format="%y %I:%M:%S%p (ms=%l us=%u ns=%n)",
3197+
fast_strftime=fast_strftime)
3198+
assert formatted[0] == "03 12:01:01PM (ms=123 us=123456 ns=123456789)"
3199+
assert formatted[1] == "03 12:01:01PM (ms=123 us=123456 ns=123456790)"
3200+
3201+
p = pd.period_range("2003-01-01 12:01:01.123456", periods=2, freq="u")
3202+
formatted = p.format(date_format="%y %I:%M:%S%p (ms=%l us=%u ns=%n)",
3203+
fast_strftime=fast_strftime)
3204+
assert formatted[0] == "03 12:01:01PM (ms=123 us=123456 ns=123456000)"
3205+
assert formatted[1] == "03 12:01:01PM (ms=123 us=123457 ns=123457000)"
3206+
3207+
def test_period_tz(self):
3208+
"""Test formatting periods created from a datetime with timezone"""
3209+
3210+
# This timestamp is in 2013 in Europe/Paris but is 2012 in UTC
3211+
dt = pd.to_datetime(["2013-01-01 00:00:00+01:00"], utc=True)
3212+
3213+
# Converting to a period looses the timezone information
3214+
# Since tz is currently set as utc, we'll see 2012
3215+
p = dt.to_period(freq="H")
3216+
assert p.format()[0] == "2012-12-31 23:00"
3217+
3218+
# If tz is currently set as paris before conversion, we'll see 2013
3219+
dt = dt.tz_convert("Europe/Paris")
3220+
p = dt.to_period(freq="H")
3221+
assert p.format()[0] == "2013-01-01 00:00"
3222+
3223+
31723224
class TestDatetimeIndexFormat:
31733225
def test_datetime(self):
31743226
formatted = pd.to_datetime([datetime(2003, 1, 1, 12), NaT]).format()

0 commit comments

Comments
 (0)