Skip to content

Commit 4b6c5ae

Browse files
committed
Adds alternative formatter
1 parent 9d5057f commit 4b6c5ae

File tree

4 files changed

+369
-1
lines changed

4 files changed

+369
-1
lines changed

pendulum/formatting/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# -*- coding: utf-8 -*-
22

33
from .classic_formatter import ClassicFormatter
4+
from .alternative_formatter import AlternativeFormatter
45

56

67
FORMATTERS = {
7-
'classic': ClassicFormatter()
8+
'classic': ClassicFormatter(),
9+
'alternative': AlternativeFormatter(),
810
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import re
4+
import datetime
5+
6+
from .formatter import Formatter
7+
8+
9+
class AlternativeFormatter(Formatter):
10+
11+
_TOKENS = '\[([^\[]*)\]|\\\(.)|' \
12+
'(' \
13+
'Mo|MM?M?M?' \
14+
'|Do|DDDo|DD?D?D?|ddd?d?|do?' \
15+
'|w[o|w]?|W[o|W]?|Qo?' \
16+
'|YYYY|YY|Y' \
17+
'|gg(ggg?)?|GG(GGG?)?' \
18+
'|e|E|a|A' \
19+
'|hh?|HH?|kk?' \
20+
'|mm?|ss?|S{1,9}' \
21+
'|x|X' \
22+
'|zz?|ZZ?' \
23+
')'
24+
25+
_FORMAT_RE = re.compile(_TOKENS)
26+
27+
_LOCALIZABLE_TOKENS = (
28+
'Mo', 'MMM', 'MMMM',
29+
'Qo',
30+
'Do',
31+
'DDDo',
32+
'do', 'dd', 'ddd', 'dddd',
33+
'wo',
34+
'Wo',
35+
'A', 'a',
36+
)
37+
38+
_TOKENS_RULES = {
39+
# Year
40+
'YYYY': lambda dt: '{:d}'.format(dt.year),
41+
'YY': lambda dt: '{:d}'.format(dt.year)[2:],
42+
'Y': lambda dt: '{:d}'.format(dt.year),
43+
44+
# Quarter
45+
'Q': lambda dt: '{:d}'.format(dt.quarter),
46+
47+
# Month
48+
'MM': lambda dt: '{:02d}'.format(dt.month),
49+
'M': lambda dt: '{:d}'.format(dt.month),
50+
51+
# Day
52+
'DD': lambda dt: '{:02d}'.format(dt.day),
53+
'D': lambda dt: '{:d}'.format(dt.day),
54+
55+
# Day of Year
56+
'DDDD': lambda dt: '{:03d}'.format(dt.day_of_year),
57+
'DDD': lambda dt: '{:d}'.format(dt.day_of_year),
58+
59+
# Day of Week
60+
'd': lambda dt: '{:d}'.format(dt.day_of_week),
61+
62+
# Hour
63+
'HH': lambda dt: '{:02d}'.format(dt.hour),
64+
'H': lambda dt: '{:d}'.format(dt.hour),
65+
'hh': lambda dt: '{:02d}'.format(dt.hour % 12 or 12),
66+
'h': lambda dt: '{:d}'.format(dt.hour % 12 or 12),
67+
68+
# Minute
69+
'mm': lambda dt: '{:02d}'.format(dt.minute),
70+
'm': lambda dt: '{:d}'.format(dt.minute),
71+
72+
# Second
73+
'ss': lambda dt: '{:02d}'.format(dt.second),
74+
's': lambda dt: '{:d}'.format(dt.second),
75+
76+
# Fractional second
77+
'S': lambda dt: '{:1d}'.format(dt.microsecond // 100000),
78+
'SS': lambda dt: '{:2d}'.format(dt.microsecond // 10000),
79+
'SSS': lambda dt: '{:3d}'.format(dt.microsecond // 1000),
80+
'SSSS': lambda dt: '{:4d}'.format(dt.microsecond // 100),
81+
'SSSSS': lambda dt: '{:5d}'.format(dt.microsecond // 10),
82+
'SSSSSS': lambda dt: '{:6d}'.format(dt.microsecond),
83+
84+
# Timestamp
85+
'X': lambda dt: '{:d}'.format(dt.timestamp),
86+
87+
# Timezone
88+
'z': lambda dt: '{}'.format(dt.tzinfo.abbrev),
89+
'zz': lambda dt: '{}'.format(dt.timezone_name),
90+
}
91+
92+
def format(self, dt, fmt, locale=None):
93+
"""
94+
Formats a Pendulum instance with a given format and locale.
95+
96+
:param dt: The instance to format
97+
:type dt: Pendulum
98+
99+
:param fmt: The format to use
100+
:type fmt: str
101+
102+
:param locale: The locale to use
103+
:type locale: str or None
104+
105+
:rtype: str
106+
"""
107+
if not locale:
108+
locale = dt.get_locale()
109+
110+
return self._FORMAT_RE.sub(
111+
lambda m: m.group(1)
112+
if m.group(1)
113+
else m.group(2)
114+
if m.group(2)
115+
else self._format_token(dt, m.group(3), locale),
116+
fmt
117+
)
118+
119+
def _format_token(self, dt, token, locale):
120+
"""
121+
Formats a Pendulum instance with a given token and locale.
122+
123+
:param dt: The instance to format
124+
:type dt: Pendulum
125+
126+
:param token: The token to use
127+
:type token: str
128+
129+
:param locale: The locale to use
130+
:type locale: str or None
131+
132+
:rtype: str
133+
"""
134+
if token in self._LOCALIZABLE_TOKENS:
135+
return self._format_localizable_token(dt, token, locale)
136+
137+
if token in self._TOKENS_RULES:
138+
return self._TOKENS_RULES[token](dt)
139+
140+
# Timezone
141+
if token in ['ZZ', 'Z']:
142+
separator = ':' if token == 'ZZ' else ''
143+
offset = dt.utcoffset() or datetime.timedelta()
144+
minutes = offset.total_seconds() / 60
145+
146+
if minutes >= 0:
147+
sign = '+'
148+
else:
149+
sign = '-'
150+
151+
hour, minute = divmod(abs(int(minutes)), 60)
152+
153+
return '{}{:02d}{}{:02d}'.format(sign, hour, separator, minute)
154+
155+
return token
156+
157+
def _format_localizable_token(self, dt, token, locale):
158+
"""
159+
Formats a Pendulum instance
160+
with a given localizable token and locale.
161+
162+
:param dt: The instance to format
163+
:type dt: Pendulum
164+
165+
:param token: The token to use
166+
:type token: str
167+
168+
:param locale: The locale to use
169+
:type locale: str or None
170+
171+
:rtype: str
172+
"""
173+
if token == 'MMM':
174+
count = dt.month
175+
trans_id = 'months_abbrev'
176+
elif token == 'MMMM':
177+
count = dt.month
178+
trans_id = 'months'
179+
elif token in ('dd', 'ddd'):
180+
count = dt.day_of_week
181+
trans_id = 'days_abbrev'
182+
elif token == 'dddd':
183+
count = dt.day_of_week
184+
trans_id = 'days'
185+
elif token == 'Do':
186+
count = dt.day
187+
trans_id = 'ordinal'
188+
elif token == 'do':
189+
count = dt.day_of_week
190+
trans_id = 'ordinal'
191+
elif token == 'Mo':
192+
count = dt.month
193+
trans_id = 'ordinal'
194+
elif token == 'Qo':
195+
count = dt.quarter
196+
trans_id = 'ordinal'
197+
elif token == 'wo':
198+
count = dt.week_of_year
199+
trans_id = 'ordinal'
200+
elif token == 'DDDo':
201+
count = dt.day_of_year
202+
trans_id = 'ordinal'
203+
elif token == 'A':
204+
count = dt.hour
205+
trans_id = 'meridian'
206+
else:
207+
return token
208+
209+
trans = dt.translator().transchoice(trans_id, count, locale=locale)
210+
211+
if trans_id == 'ordinal':
212+
return '{:d}{}'.format(count, trans)
213+
214+
return trans

pendulum/pendulum.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,11 @@ def format(self, fmt, locale=None, formatter=None):
899899
:rtype: str
900900
"""
901901
if formatter is None:
902+
message = 'format() will default to the [alternative] formatter ' \
903+
'in the next major version. If you want to keep ' \
904+
'the current behavior in the future add the "formatter" ' \
905+
'keyword argument: format(formatter=\'classic\')'
906+
warnings.warn(message, DeprecationWarning, 2)
902907
formatter = 'classic'
903908

904909
if formatter not in FORMATTERS:
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from pendulum import Pendulum
4+
from pendulum.formatting.alternative_formatter import AlternativeFormatter
5+
from .. import AbstractTestCase
6+
7+
8+
class ClassicFormatterTest(AbstractTestCase):
9+
10+
def test_year_tokens(self):
11+
d = Pendulum(2009, 1, 14, 15, 25, 50, 123456)
12+
f = AlternativeFormatter()
13+
self.assertEqual('2009', f.format(d, 'YYYY'))
14+
self.assertEqual('09', f.format(d, 'YY'))
15+
self.assertEqual('2009', f.format(d, 'Y'))
16+
17+
def test_quarter_tokens(self):
18+
f = AlternativeFormatter()
19+
d = Pendulum(1985, 1, 4)
20+
self.assertEqual('1', f.format(d, 'Q'))
21+
d = Pendulum(2029, 8, 1)
22+
self.assertEqual('3', f.format(d, 'Q'))
23+
d = Pendulum(1985, 1, 4)
24+
self.assertEqual('1st', f.format(d, 'Qo'))
25+
d = Pendulum(2029, 8, 1)
26+
self.assertEqual('3rd', f.format(d, 'Qo'))
27+
d = Pendulum(1985, 1, 4)
28+
self.assertEqual('1er', f.format(d, 'Qo', locale='fr'))
29+
d = Pendulum(2029, 8, 1)
30+
self.assertEqual('3e', f.format(d, 'Qo', locale='fr'))
31+
32+
def test_month_tokens(self):
33+
f = AlternativeFormatter()
34+
d = Pendulum(2016, 3, 24)
35+
self.assertEqual('03', f.format(d, 'MM'))
36+
self.assertEqual('3', f.format(d, 'M'))
37+
38+
self.assertEqual('Mar', f.format(d, 'MMM'))
39+
self.assertEqual('March', f.format(d, 'MMMM'))
40+
self.assertEqual('3rd', f.format(d, 'Mo'))
41+
42+
self.assertEqual('mars', f.format(d, 'MMM', locale='fr'))
43+
self.assertEqual('mars', f.format(d, 'MMMM', locale='fr'))
44+
self.assertEqual('3e', f.format(d, 'Mo', locale='fr'))
45+
46+
def test_day_tokens(self):
47+
f = AlternativeFormatter()
48+
d = Pendulum(2016, 3, 7)
49+
self.assertEqual('07', f.format(d, 'DD'))
50+
self.assertEqual('7', f.format(d, 'D'))
51+
52+
self.assertEqual('7th', f.format(d, 'Do'))
53+
self.assertEqual('1st', f.format(d.first_of('month'), 'Do'))
54+
55+
self.assertEqual('7e', f.format(d, 'Do', locale='fr'))
56+
self.assertEqual('1er', f.format(d.first_of('month'), 'Do', locale='fr'))
57+
58+
def test_day_of_year(self):
59+
f = AlternativeFormatter()
60+
d = Pendulum(2016, 8, 28)
61+
self.assertEqual('241', f.format(d, 'DDDD'))
62+
self.assertEqual('241', f.format(d, 'DDD'))
63+
self.assertEqual('001', f.format(d.start_of('year'), 'DDDD'))
64+
self.assertEqual('1', f.format(d.start_of('year'), 'DDD'))
65+
66+
self.assertEqual('241st', f.format(d, 'DDDo'))
67+
self.assertEqual('244th', f.format(d.add(days=3), 'DDDo'))
68+
69+
self.assertEqual('241e', f.format(d, 'DDDo', locale='fr'))
70+
self.assertEqual('244e', f.format(d.add(days=3), 'DDDo', locale='fr'))
71+
72+
def test_day_of_week(self):
73+
f = AlternativeFormatter()
74+
d = Pendulum(2016, 8, 28)
75+
self.assertEqual('0', f.format(d, 'd'))
76+
77+
self.assertEqual('Sun', f.format(d, 'dd'))
78+
self.assertEqual('Sun', f.format(d, 'ddd'))
79+
self.assertEqual('Sunday', f.format(d, 'dddd'))
80+
81+
self.assertEqual('dim', f.format(d, 'dd', locale='fr'))
82+
self.assertEqual('dim', f.format(d, 'ddd', locale='fr'))
83+
self.assertEqual('dimanche', f.format(d, 'dddd', locale='fr'))
84+
85+
def test_am_pm(self):
86+
f = AlternativeFormatter()
87+
d = Pendulum(2016, 8, 28, 23)
88+
self.assertEqual('PM', f.format(d, 'A'))
89+
self.assertEqual('AM', f.format(d.hour_(11), 'A'))
90+
91+
def test_hour(self):
92+
f = AlternativeFormatter()
93+
d = Pendulum(2016, 8, 28, 7)
94+
self.assertEqual('7', f.format(d, 'H'))
95+
self.assertEqual('07', f.format(d, 'HH'))
96+
97+
d = Pendulum(2016, 8, 28, 0)
98+
self.assertEqual('12', f.format(d, 'h'))
99+
self.assertEqual('12', f.format(d, 'hh'))
100+
101+
def test_minute(self):
102+
f = AlternativeFormatter()
103+
d = Pendulum(2016, 8, 28, 7, 3)
104+
self.assertEqual('3', f.format(d, 'm'))
105+
self.assertEqual('03', f.format(d, 'mm'))
106+
107+
def test_second(self):
108+
f = AlternativeFormatter()
109+
d = Pendulum(2016, 8, 28, 7, 3, 6)
110+
self.assertEqual('6', f.format(d, 's'))
111+
self.assertEqual('06', f.format(d, 'ss'))
112+
113+
def test_fractional_second(self):
114+
f = AlternativeFormatter()
115+
d = Pendulum(2016, 8, 28, 7, 3, 6, 123456)
116+
self.assertEqual('1', f.format(d, 'S'))
117+
self.assertEqual('12', f.format(d, 'SS'))
118+
self.assertEqual('123', f.format(d, 'SSS'))
119+
self.assertEqual('1234', f.format(d, 'SSSS'))
120+
self.assertEqual('12345', f.format(d, 'SSSSS'))
121+
self.assertEqual('123456', f.format(d, 'SSSSSS'))
122+
123+
def test_timezone(self):
124+
f = AlternativeFormatter()
125+
d = Pendulum(2016, 8, 28, 7, 3, 6, 123456, 'Europe/Paris')
126+
self.assertEqual('CEST', f.format(d, 'z'))
127+
self.assertEqual('Europe/Paris', f.format(d, 'zz'))
128+
129+
d = Pendulum(2016, 1, 28, 7, 3, 6, 123456, 'Europe/Paris')
130+
self.assertEqual('CET', f.format(d, 'z'))
131+
self.assertEqual('Europe/Paris', f.format(d, 'zz'))
132+
133+
def test_timezone_offset(self):
134+
f = AlternativeFormatter()
135+
d = Pendulum(2016, 8, 28, 7, 3, 6, 123456, 'Europe/Paris')
136+
self.assertEqual('+0200', f.format(d, 'Z'))
137+
self.assertEqual('+02:00', f.format(d, 'ZZ'))
138+
139+
d = Pendulum(2016, 1, 28, 7, 3, 6, 123456, 'Europe/Paris')
140+
self.assertEqual('+0100', f.format(d, 'Z'))
141+
self.assertEqual('+01:00', f.format(d, 'ZZ'))
142+
143+
def test_timestamp(self):
144+
f = AlternativeFormatter()
145+
d = Pendulum(1970, 1, 1)
146+
self.assertEqual('0', f.format(d, 'X'))
147+
self.assertEqual('86400', f.format(d.add(days=1), 'X'))

0 commit comments

Comments
 (0)