Skip to content

Commit b5085fe

Browse files
author
Sylvain MARIE
committed
Fixed pandas-dev#46252. Added comments and updated strftime doc for maintenance. Added a TestPeriodIndexFormat class
1 parent 3cc7fac commit b5085fe

File tree

2 files changed

+108
-17
lines changed

2 files changed

+108
-17
lines changed

pandas/_libs/tslibs/period.pyx

+37-15
Original file line numberDiff line numberDiff line change
@@ -1236,14 +1236,16 @@ cdef list extra_fmts = [(b"%q", b"^`AB`^"),
12361236
cdef list str_extra_fmts = ["^`AB`^", "^`CD`^", "^`EF`^",
12371237
"^`GH`^", "^`IJ`^", "^`KL`^"]
12381238

1239+
cdef inline int idx_first_nonfiscal_fmt = 3
1240+
12391241
cdef str _period_strftime(int64_t value, int freq, bytes fmt):
12401242
cdef:
12411243
Py_ssize_t i
12421244
npy_datetimestruct dts
12431245
char *formatted
12441246
bytes pat, brepl
12451247
list found_pat = [False] * len(extra_fmts)
1246-
int quarter
1248+
int quarter, us, ps
12471249
str result, repl
12481250

12491251
get_date_info(value, freq, &dts)
@@ -1263,23 +1265,33 @@ cdef str _period_strftime(int64_t value, int freq, bytes fmt):
12631265
result = util.char_to_string(formatted)
12641266
free(formatted)
12651267

1268+
# Now we will fill the placeholders corresponding to our additional directives
1269+
# First prepare the contents
1270+
if any(found_pat[idx_first_nonfiscal_fmt:]):
1271+
# Save these to local vars as dts can be modified by get_yq below
1272+
us = dts.us
1273+
ps = dts.ps
1274+
if any(found_pat[0:idx_first_nonfiscal_fmt]):
1275+
# Note: this modifies `dts` in-place so that year becomes fiscal year
1276+
# However it looses the us and ps
1277+
quarter = get_yq(value, freq, &dts)
1278+
1279+
# Now do the filling per se
12661280
for i in range(len(extra_fmts)):
12671281
if found_pat[i]:
12681282

1269-
quarter = get_yq(value, freq, &dts)
1270-
1271-
if i == 0:
1272-
repl = str(quarter)
1273-
elif i == 1: # %f, 2-digit year
1283+
if i == 0: # %q, 1-digit quarter.
1284+
repl = f"{quarter}"
1285+
elif i == 1: # %f, 2-digit 'Fiscal' year
12741286
repl = f"{(dts.year % 100):02d}"
1275-
elif i == 2:
1287+
elif i == 2: # %F, 'Fiscal' year with a century
12761288
repl = str(dts.year)
1277-
elif i == 3:
1278-
repl = f"{(value % 1_000):03d}"
1279-
elif i == 4:
1280-
repl = f"{(value % 1_000_000):06d}"
1281-
elif i == 5:
1282-
repl = f"{(value % 1_000_000_000):09d}"
1289+
elif i == 3: # %l, milliseconds
1290+
repl = f"{(us // 1_000):03d}"
1291+
elif i == 4: # %u, microseconds
1292+
repl = f"{(us):06d}"
1293+
elif i == 5: # %n, nanoseconds
1294+
repl = f"{((us * 1000) + (ps // 1000)):09d}"
12831295

12841296
result = result.replace(str_extra_fmts[i], repl)
12851297

@@ -2332,7 +2344,8 @@ cdef class _Period(PeriodMixin):
23322344
containing one or several directives. The method recognizes the same
23332345
directives as the :func:`time.strftime` function of the standard Python
23342346
distribution, as well as the specific additional directives ``%f``,
2335-
``%F``, ``%q``. (formatting & docs originally from scikits.timeries).
2347+
``%F``, ``%q``, ``%l``, ``%u``, ``%n``.
2348+
(formatting & docs originally from scikits.timeries).
23362349

23372350
+-----------+--------------------------------+-------+
23382351
| Directive | Meaning | Notes |
@@ -2379,11 +2392,20 @@ cdef class _Period(PeriodMixin):
23792392
| | AM or PM. | |
23802393
+-----------+--------------------------------+-------+
23812394
| ``%q`` | Quarter as a decimal number | |
2382-
| | [01,04] | |
2395+
| | [1,4] | |
23832396
+-----------+--------------------------------+-------+
23842397
| ``%S`` | Second as a decimal number | \(4) |
23852398
| | [00,61]. | |
23862399
+-----------+--------------------------------+-------+
2400+
| ``%l`` | Millisecond as a decimal number| |
2401+
| | [000,999]. | |
2402+
+-----------+--------------------------------+-------+
2403+
| ``%u`` | Microsecond as a decimal number| |
2404+
| | [000000,999999]. | |
2405+
+-----------+--------------------------------+-------+
2406+
| ``%n`` | Nanosecond as a decimal number | |
2407+
| | [000000000,999999999]. | |
2408+
+-----------+--------------------------------+-------+
23872409
| ``%U`` | Week number of the year | \(5) |
23882410
| | (Sunday as the first day of | |
23892411
| | the week) as a decimal number | |

pandas/tests/io/formats/test_format.py

+71-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""
22
Test output formatting for Series/DataFrame, including to_string & reprs
33
"""
4-
5-
from datetime import datetime
4+
from datetime import (
5+
datetime,
6+
time,
7+
)
68
from io import StringIO
79
import itertools
810
from operator import methodcaller
@@ -47,6 +49,13 @@
4749
use_32bit_repr = is_platform_windows() or not IS64
4850

4951

52+
def get_local_am_pm():
53+
"""Return the AM and PM strings returned by strftime in current locale"""
54+
am_local = time(1).strftime("%p")
55+
pm_local = time(13).strftime("%p")
56+
return am_local, pm_local
57+
58+
5059
@pytest.fixture(params=["string", "pathlike", "buffer"])
5160
def filepath_or_buffer_id(request):
5261
"""
@@ -3167,6 +3176,66 @@ def test_str(self):
31673176
assert str(NaT) == "NaT"
31683177

31693178

3179+
class TestPeriodIndexFormat:
3180+
def test_period(self):
3181+
"""Basic test for period formatting with default format."""
3182+
p = pd.PeriodIndex([datetime(2003, 1, 1, 12), None], freq="H")
3183+
# format is equivalent to strftime(None)
3184+
formatted = p.format()
3185+
assert formatted[0] == p[0].strftime(None)
3186+
assert formatted[0] == "2003-01-01 12:00" # default: minutes not shown
3187+
assert formatted[1] == "NaT"
3188+
3189+
p = pd.period_range("2003-01-01 12:01:01.123456789", periods=2, freq="n")
3190+
# format is equivalent to strftime(None)
3191+
formatted = p.format()
3192+
assert formatted[0] == p[0].strftime(None)
3193+
assert formatted[0] == "2003-01-01 12:01:01.123456789"
3194+
assert formatted[1] == "2003-01-01 12:01:01.123456790"
3195+
3196+
def test_period_custom(self):
3197+
# GH46252
3198+
3199+
# Get locale-specific reference
3200+
am_local, pm_local = get_local_am_pm()
3201+
3202+
# 3 digits
3203+
p = pd.period_range("2003-01-01 12:01:01.123", periods=2, freq="l")
3204+
formatted = p.format(date_format="%y %I:%M:%S%p (ms=%l us=%u ns=%n)")
3205+
assert formatted[0] == f"03 12:01:01{pm_local} (ms=123 us=123000 ns=123000000)"
3206+
assert formatted[1] == f"03 12:01:01{pm_local} (ms=124 us=124000 ns=124000000)"
3207+
3208+
# 6 digits
3209+
p = pd.period_range("2003-01-01 12:01:01.123456", periods=2, freq="u")
3210+
formatted = p.format(date_format="%y %I:%M:%S%p (ms=%l us=%u ns=%n)")
3211+
assert formatted[0] == f"03 12:01:01{pm_local} (ms=123 us=123456 ns=123456000)"
3212+
assert formatted[1] == f"03 12:01:01{pm_local} (ms=123 us=123457 ns=123457000)"
3213+
3214+
# 9 digits
3215+
p = pd.period_range("2003-01-01 12:01:01.123456789", periods=2, freq="n")
3216+
formatted = p.format(date_format="%y %I:%M:%S%p (ms=%l us=%u ns=%n)")
3217+
assert formatted[0] == f"03 12:01:01{pm_local} (ms=123 us=123456 ns=123456789)"
3218+
assert formatted[1] == f"03 12:01:01{pm_local} (ms=123 us=123456 ns=123456790)"
3219+
3220+
def test_period_tz(self):
3221+
"""Test formatting periods created from a datetime with timezone."""
3222+
3223+
# This timestamp is in 2013 in Europe/Paris but is 2012 in UTC
3224+
dt = pd.to_datetime(["2013-01-01 00:00:00+01:00"], utc=True)
3225+
3226+
# Converting to a period looses the timezone information
3227+
# Since tz is currently set as utc, we'll see 2012
3228+
with tm.assert_produces_warning(UserWarning, match="will drop timezone"):
3229+
p = dt.to_period(freq="H")
3230+
assert p.format()[0] == "2012-12-31 23:00"
3231+
3232+
# If tz is currently set as paris before conversion, we'll see 2013
3233+
dt = dt.tz_convert("Europe/Paris")
3234+
with tm.assert_produces_warning(UserWarning, match="will drop timezone"):
3235+
p = dt.to_period(freq="H")
3236+
assert p.format()[0] == "2013-01-01 00:00"
3237+
3238+
31703239
class TestDatetimeIndexFormat:
31713240
def test_datetime(self):
31723241
formatted = pd.to_datetime([datetime(2003, 1, 1, 12), NaT]).format()

0 commit comments

Comments
 (0)