1"""
2PHP date() style date formatting
3See https://www.php.net/date for format strings
4
5Usage:
6>>> from datetime import datetime
7>>> d = datetime.now()
8>>> df = DateFormat(d)
9>>> print(df.format('jS F Y H:i'))
107th October 2003 11:39
11>>>
12"""
13
14import calendar
15from datetime import date, datetime, time
16from email.utils import format_datetime as format_datetime_rfc5322
17
18from django.utils.dates import (
19 MONTHS,
20 MONTHS_3,
21 MONTHS_ALT,
22 MONTHS_AP,
23 WEEKDAYS,
24 WEEKDAYS_ABBR,
25)
26from django.utils.regex_helper import _lazy_re_compile
27from django.utils.timezone import (
28 _datetime_ambiguous_or_imaginary,
29 get_default_timezone,
30 is_naive,
31 make_aware,
32)
33from django.utils.translation import gettext as _
34
35re_formatchars = _lazy_re_compile(r"(?<!\\)([aAbcdDeEfFgGhHiIjlLmMnNoOPrsStTUuwWyYzZ])")
36re_escaped = _lazy_re_compile(r"\\(.)")
37
38
39class Formatter:
40 def format(self, formatstr):
41 pieces = []
42 for i, piece in enumerate(re_formatchars.split(str(formatstr))):
43 if i % 2:
44 if type(self.data) is date and hasattr(TimeFormat, piece):
45 raise TypeError(
46 "The format for date objects may not contain "
47 "time-related format specifiers (found '%s')." % piece
48 )
49 pieces.append(str(getattr(self, piece)()))
50 elif piece:
51 pieces.append(re_escaped.sub(r"\1", piece))
52 return "".join(pieces)
53
54
55class TimeFormat(Formatter):
56 def __init__(self, obj):
57 self.data = obj
58 self.timezone = None
59
60 if isinstance(obj, datetime):
61 # Timezone is only supported when formatting datetime objects, not
62 # date objects (timezone information not appropriate), or time
63 # objects (against established django policy).
64 if is_naive(obj):
65 timezone = get_default_timezone()
66 else:
67 timezone = obj.tzinfo
68 if not _datetime_ambiguous_or_imaginary(obj, timezone):
69 self.timezone = timezone
70
71 def a(self):
72 "'a.m.' or 'p.m.'"
73 if self.data.hour > 11:
74 return _("p.m.")
75 return _("a.m.")
76
77 def A(self):
78 "'AM' or 'PM'"
79 if self.data.hour > 11:
80 return _("PM")
81 return _("AM")
82
83 def e(self):
84 """
85 Timezone name.
86
87 If timezone information is not available, return an empty string.
88 """
89 if not self.timezone:
90 return ""
91
92 try:
93 if getattr(self.data, "tzinfo", None):
94 return self.data.tzname() or ""
95 except NotImplementedError:
96 pass
97 return ""
98
99 def f(self):
100 """
101 Time, in 12-hour hours and minutes, with minutes left off if they're
102 zero.
103 Examples: '1', '1:30', '2:05', '2'
104 Proprietary extension.
105 """
106 hour = self.data.hour % 12 or 12
107 minute = self.data.minute
108 return "%d:%02d" % (hour, minute) if minute else hour
109
110 def g(self):
111 "Hour, 12-hour format without leading zeros; i.e. '1' to '12'"
112 return self.data.hour % 12 or 12
113
114 def G(self):
115 "Hour, 24-hour format without leading zeros; i.e. '0' to '23'"
116 return self.data.hour
117
118 def h(self):
119 "Hour, 12-hour format; i.e. '01' to '12'"
120 return "%02d" % (self.data.hour % 12 or 12)
121
122 def H(self):
123 "Hour, 24-hour format; i.e. '00' to '23'"
124 return "%02d" % self.data.hour
125
126 def i(self):
127 "Minutes; i.e. '00' to '59'"
128 return "%02d" % self.data.minute
129
130 def O(self): # NOQA: E743, E741
131 """
132 Difference to Greenwich time in hours; e.g. '+0200', '-0430'.
133
134 If timezone information is not available, return an empty string.
135 """
136 if self.timezone is None:
137 return ""
138
139 offset = self.timezone.utcoffset(self.data)
140 seconds = offset.days * 86400 + offset.seconds
141 sign = "-" if seconds < 0 else "+"
142 seconds = abs(seconds)
143 return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60)
144
145 def P(self):
146 """
147 Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
148 if they're zero and the strings 'midnight' and 'noon' if appropriate.
149 Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
150 Proprietary extension.
151 """
152 if self.data.minute == 0 and self.data.hour == 0:
153 return _("midnight")
154 if self.data.minute == 0 and self.data.hour == 12:
155 return _("noon")
156 return "%s %s" % (self.f(), self.a())
157
158 def s(self):
159 "Seconds; i.e. '00' to '59'"
160 return "%02d" % self.data.second
161
162 def T(self):
163 """
164 Time zone of this machine; e.g. 'EST' or 'MDT'.
165
166 If timezone information is not available, return an empty string.
167 """
168 if self.timezone is None:
169 return ""
170
171 return str(self.timezone.tzname(self.data))
172
173 def u(self):
174 "Microseconds; i.e. '000000' to '999999'"
175 return "%06d" % self.data.microsecond
176
177 def Z(self):
178 """
179 Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
180 timezones west of UTC is always negative, and for those east of UTC is
181 always positive.
182
183 If timezone information is not available, return an empty string.
184 """
185 if self.timezone is None:
186 return ""
187
188 offset = self.timezone.utcoffset(self.data)
189
190 # `offset` is a datetime.timedelta. For negative values (to the west of
191 # UTC) only days can be negative (days=-1) and seconds are always
192 # positive. e.g. UTC-1 -> timedelta(days=-1, seconds=82800, microseconds=0)
193 # Positive offsets have days=0
194 return offset.days * 86400 + offset.seconds
195
196
197class DateFormat(TimeFormat):
198 def b(self):
199 "Month, textual, 3 letters, lowercase; e.g. 'jan'"
200 return MONTHS_3[self.data.month]
201
202 def c(self):
203 """
204 ISO 8601 Format
205 Example : '2008-01-02T10:30:00.000123'
206 """
207 return self.data.isoformat()
208
209 def d(self):
210 "Day of the month, 2 digits with leading zeros; i.e. '01' to '31'"
211 return "%02d" % self.data.day
212
213 def D(self):
214 "Day of the week, textual, 3 letters; e.g. 'Fri'"
215 return WEEKDAYS_ABBR[self.data.weekday()]
216
217 def E(self):
218 "Alternative month names as required by some locales. Proprietary extension."
219 return MONTHS_ALT[self.data.month]
220
221 def F(self):
222 "Month, textual, long; e.g. 'January'"
223 return MONTHS[self.data.month]
224
225 def I(self): # NOQA: E743, E741
226 "'1' if daylight saving time, '0' otherwise."
227 if self.timezone is None:
228 return ""
229 return "1" if self.timezone.dst(self.data) else "0"
230
231 def j(self):
232 "Day of the month without leading zeros; i.e. '1' to '31'"
233 return self.data.day
234
235 def l(self): # NOQA: E743, E741
236 "Day of the week, textual, long; e.g. 'Friday'"
237 return WEEKDAYS[self.data.weekday()]
238
239 def L(self):
240 "Boolean for whether it is a leap year; i.e. True or False"
241 return calendar.isleap(self.data.year)
242
243 def m(self):
244 "Month; i.e. '01' to '12'"
245 return "%02d" % self.data.month
246
247 def M(self):
248 "Month, textual, 3 letters; e.g. 'Jan'"
249 return MONTHS_3[self.data.month].title()
250
251 def n(self):
252 "Month without leading zeros; i.e. '1' to '12'"
253 return self.data.month
254
255 def N(self):
256 "Month abbreviation in Associated Press style. Proprietary extension."
257 return MONTHS_AP[self.data.month]
258
259 def o(self):
260 "ISO 8601 year number matching the ISO week number (W)"
261 return self.data.isocalendar().year
262
263 def r(self):
264 "RFC 5322 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'"
265 value = self.data
266 if not isinstance(value, datetime):
267 # Assume midnight in default timezone if datetime.date provided.
268 default_timezone = get_default_timezone()
269 value = datetime.combine(value, time.min).replace(tzinfo=default_timezone)
270 elif is_naive(value):
271 value = make_aware(value, timezone=self.timezone)
272 return format_datetime_rfc5322(value)
273
274 def S(self):
275 """
276 English ordinal suffix for the day of the month, 2 characters; i.e.
277 'st', 'nd', 'rd' or 'th'.
278 """
279 if self.data.day in (11, 12, 13): # Special case
280 return "th"
281 last = self.data.day % 10
282 if last == 1:
283 return "st"
284 if last == 2:
285 return "nd"
286 if last == 3:
287 return "rd"
288 return "th"
289
290 def t(self):
291 "Number of days in the given month; i.e. '28' to '31'"
292 return calendar.monthrange(self.data.year, self.data.month)[1]
293
294 def U(self):
295 "Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)"
296 value = self.data
297 if not isinstance(value, datetime):
298 value = datetime.combine(value, time.min)
299 return int(value.timestamp())
300
301 def w(self):
302 "Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)"
303 return (self.data.weekday() + 1) % 7
304
305 def W(self):
306 "ISO-8601 week number of year, weeks starting on Monday"
307 return self.data.isocalendar().week
308
309 def y(self):
310 """Year, 2 digits with leading zeros; e.g. '99'."""
311 return "%02d" % (self.data.year % 100)
312
313 def Y(self):
314 """Year, 4 digits with leading zeros; e.g. '1999'."""
315 return "%04d" % self.data.year
316
317 def z(self):
318 """Day of the year, i.e. 1 to 366."""
319 return self.data.timetuple().tm_yday
320
321
322def format(value, format_string):
323 "Convenience function"
324 df = DateFormat(value)
325 return df.format(format_string)
326
327
328def time_format(value, format_string):
329 "Convenience function"
330 tf = TimeFormat(value)
331 return tf.format(format_string)