Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pendulum/formatting/formatter.py: 16%
275 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
4import datetime
5import re
6import typing
8import pendulum
10from pendulum.locales.locale import Locale
11from pendulum.utils._compat import decode
14_MATCH_1 = r"\d"
15_MATCH_2 = r"\d\d"
16_MATCH_3 = r"\d{3}"
17_MATCH_4 = r"\d{4}"
18_MATCH_6 = r"[+-]?\d{6}"
19_MATCH_1_TO_2 = r"\d\d?"
20_MATCH_1_TO_2_LEFT_PAD = r"[0-9 ]\d?"
21_MATCH_1_TO_3 = r"\d{1,3}"
22_MATCH_1_TO_4 = r"\d{1,4}"
23_MATCH_1_TO_6 = r"[+-]?\d{1,6}"
24_MATCH_3_TO_4 = r"\d{3}\d?"
25_MATCH_5_TO_6 = r"\d{5}\d?"
26_MATCH_UNSIGNED = r"\d+"
27_MATCH_SIGNED = r"[+-]?\d+"
28_MATCH_OFFSET = r"[Zz]|[+-]\d\d:?\d\d"
29_MATCH_SHORT_OFFSET = r"[Zz]|[+-]\d\d(?::?\d\d)?"
30_MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?"
31_MATCH_WORD = (
32 "(?i)[0-9]*"
33 "['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+"
34 r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}"
35)
36_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?"
39class Formatter:
41 _TOKENS = (
42 r"\[([^\[]*)\]|\\(.)|"
43 "("
44 "Mo|MM?M?M?"
45 "|Do|DDDo|DD?D?D?|ddd?d?|do?"
46 "|E{1,4}"
47 "|w[o|w]?|W[o|W]?|Qo?"
48 "|YYYY|YY|Y"
49 "|gg(ggg?)?|GG(GGG?)?"
50 "|a|A"
51 "|hh?|HH?|kk?"
52 "|mm?|ss?|S{1,9}"
53 "|x|X"
54 "|zz?|ZZ?"
55 "|LTS|LT|LL?L?L?"
56 ")"
57 )
59 _FORMAT_RE = re.compile(_TOKENS)
61 _FROM_FORMAT_RE = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
63 _LOCALIZABLE_TOKENS = {
64 "Qo": None,
65 "MMMM": "months.wide",
66 "MMM": "months.abbreviated",
67 "Mo": None,
68 "DDDo": None,
69 "Do": lambda locale: tuple(
70 r"\d+{}".format(o) for o in locale.get("custom.ordinal").values()
71 ),
72 "dddd": "days.wide",
73 "ddd": "days.abbreviated",
74 "dd": "days.short",
75 "do": None,
76 "Wo": None,
77 "wo": None,
78 "A": lambda locale: (
79 locale.translation("day_periods.am"),
80 locale.translation("day_periods.pm"),
81 ),
82 "a": lambda locale: (
83 locale.translation("day_periods.am").lower(),
84 locale.translation("day_periods.pm").lower(),
85 ),
86 }
88 _TOKENS_RULES = {
89 # Year
90 "YYYY": lambda dt: "{:d}".format(dt.year),
91 "YY": lambda dt: "{:d}".format(dt.year)[2:],
92 "Y": lambda dt: "{:d}".format(dt.year),
93 # Quarter
94 "Q": lambda dt: "{:d}".format(dt.quarter),
95 # Month
96 "MM": lambda dt: "{:02d}".format(dt.month),
97 "M": lambda dt: "{:d}".format(dt.month),
98 # Day
99 "DD": lambda dt: "{:02d}".format(dt.day),
100 "D": lambda dt: "{:d}".format(dt.day),
101 # Day of Year
102 "DDDD": lambda dt: "{:03d}".format(dt.day_of_year),
103 "DDD": lambda dt: "{:d}".format(dt.day_of_year),
104 # Day of Week
105 "d": lambda dt: "{:d}".format(dt.day_of_week),
106 # Day of ISO Week
107 "E": lambda dt: "{:d}".format(dt.isoweekday()),
108 # Hour
109 "HH": lambda dt: "{:02d}".format(dt.hour),
110 "H": lambda dt: "{:d}".format(dt.hour),
111 "hh": lambda dt: "{:02d}".format(dt.hour % 12 or 12),
112 "h": lambda dt: "{:d}".format(dt.hour % 12 or 12),
113 # Minute
114 "mm": lambda dt: "{:02d}".format(dt.minute),
115 "m": lambda dt: "{:d}".format(dt.minute),
116 # Second
117 "ss": lambda dt: "{:02d}".format(dt.second),
118 "s": lambda dt: "{:d}".format(dt.second),
119 # Fractional second
120 "S": lambda dt: "{:01d}".format(dt.microsecond // 100000),
121 "SS": lambda dt: "{:02d}".format(dt.microsecond // 10000),
122 "SSS": lambda dt: "{:03d}".format(dt.microsecond // 1000),
123 "SSSS": lambda dt: "{:04d}".format(dt.microsecond // 100),
124 "SSSSS": lambda dt: "{:05d}".format(dt.microsecond // 10),
125 "SSSSSS": lambda dt: "{:06d}".format(dt.microsecond),
126 # Timestamp
127 "X": lambda dt: "{:d}".format(dt.int_timestamp),
128 "x": lambda dt: "{:d}".format(dt.int_timestamp * 1000 + dt.microsecond // 1000),
129 # Timezone
130 "zz": lambda dt: "{}".format(dt.tzname() if dt.tzinfo is not None else ""),
131 "z": lambda dt: "{}".format(dt.timezone_name or ""),
132 }
134 _DATE_FORMATS = {
135 "LTS": "formats.time.full",
136 "LT": "formats.time.short",
137 "L": "formats.date.short",
138 "LL": "formats.date.long",
139 "LLL": "formats.datetime.long",
140 "LLLL": "formats.datetime.full",
141 }
143 _DEFAULT_DATE_FORMATS = {
144 "LTS": "h:mm:ss A",
145 "LT": "h:mm A",
146 "L": "MM/DD/YYYY",
147 "LL": "MMMM D, YYYY",
148 "LLL": "MMMM D, YYYY h:mm A",
149 "LLLL": "dddd, MMMM D, YYYY h:mm A",
150 }
152 _REGEX_TOKENS = {
153 "Y": _MATCH_SIGNED,
154 "YY": (_MATCH_1_TO_2, _MATCH_2),
155 "YYYY": (_MATCH_1_TO_4, _MATCH_4),
156 "Q": _MATCH_1,
157 "Qo": None,
158 "M": _MATCH_1_TO_2,
159 "MM": (_MATCH_1_TO_2, _MATCH_2),
160 "MMM": _MATCH_WORD,
161 "MMMM": _MATCH_WORD,
162 "D": _MATCH_1_TO_2,
163 "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2),
164 "DDD": _MATCH_1_TO_3,
165 "DDDD": _MATCH_3,
166 "dddd": _MATCH_WORD,
167 "ddd": _MATCH_WORD,
168 "dd": _MATCH_WORD,
169 "d": _MATCH_1,
170 "E": _MATCH_1,
171 "Do": None,
172 "H": _MATCH_1_TO_2,
173 "HH": (_MATCH_1_TO_2, _MATCH_2),
174 "h": _MATCH_1_TO_2,
175 "hh": (_MATCH_1_TO_2, _MATCH_2),
176 "m": _MATCH_1_TO_2,
177 "mm": (_MATCH_1_TO_2, _MATCH_2),
178 "s": _MATCH_1_TO_2,
179 "ss": (_MATCH_1_TO_2, _MATCH_2),
180 "S": (_MATCH_1_TO_3, _MATCH_1),
181 "SS": (_MATCH_1_TO_3, _MATCH_2),
182 "SSS": (_MATCH_1_TO_3, _MATCH_3),
183 "SSSS": _MATCH_UNSIGNED,
184 "SSSSS": _MATCH_UNSIGNED,
185 "SSSSSS": _MATCH_UNSIGNED,
186 "x": _MATCH_SIGNED,
187 "X": _MATCH_TIMESTAMP,
188 "ZZ": _MATCH_SHORT_OFFSET,
189 "Z": _MATCH_OFFSET,
190 "z": _MATCH_TIMEZONE,
191 }
193 _PARSE_TOKENS = {
194 "YYYY": lambda year: int(year),
195 "YY": lambda year: int(year),
196 "Q": lambda quarter: int(quarter),
197 "MMMM": lambda month: month,
198 "MMM": lambda month: month,
199 "MM": lambda month: int(month),
200 "M": lambda month: int(month),
201 "DDDD": lambda day: int(day),
202 "DDD": lambda day: int(day),
203 "DD": lambda day: int(day),
204 "D": lambda day: int(day),
205 "dddd": lambda weekday: weekday,
206 "ddd": lambda weekday: weekday,
207 "dd": lambda weekday: weekday,
208 "d": lambda weekday: int(weekday) % 7,
209 "E": lambda weekday: int(weekday),
210 "HH": lambda hour: int(hour),
211 "H": lambda hour: int(hour),
212 "hh": lambda hour: int(hour),
213 "h": lambda hour: int(hour),
214 "mm": lambda minute: int(minute),
215 "m": lambda minute: int(minute),
216 "ss": lambda second: int(second),
217 "s": lambda second: int(second),
218 "S": lambda us: int(us) * 100000,
219 "SS": lambda us: int(us) * 10000,
220 "SSS": lambda us: int(us) * 1000,
221 "SSSS": lambda us: int(us) * 100,
222 "SSSSS": lambda us: int(us) * 10,
223 "SSSSSS": lambda us: int(us),
224 "a": lambda meridiem: meridiem,
225 "X": lambda ts: float(ts),
226 "x": lambda ts: float(ts) / 1e3,
227 "ZZ": str,
228 "Z": str,
229 "z": str,
230 }
232 def format(
233 self, dt, fmt, locale=None
234 ): # type: (pendulum.DateTime, str, typing.Optional[typing.Union[str, Locale]]) -> str
235 """
236 Formats a DateTime instance with a given format and locale.
238 :param dt: The instance to format
239 :type dt: pendulum.DateTime
241 :param fmt: The format to use
242 :type fmt: str
244 :param locale: The locale to use
245 :type locale: str or Locale or None
247 :rtype: str
248 """
249 if not locale:
250 locale = pendulum.get_locale()
252 locale = Locale.load(locale)
254 result = self._FORMAT_RE.sub(
255 lambda m: m.group(1)
256 if m.group(1)
257 else m.group(2)
258 if m.group(2)
259 else self._format_token(dt, m.group(3), locale),
260 fmt,
261 )
263 return decode(result)
265 def _format_token(
266 self, dt, token, locale
267 ): # type: (pendulum.DateTime, str, Locale) -> str
268 """
269 Formats a DateTime instance with a given token and locale.
271 :param dt: The instance to format
272 :type dt: pendulum.DateTime
274 :param token: The token to use
275 :type token: str
277 :param locale: The locale to use
278 :type locale: Locale
280 :rtype: str
281 """
282 if token in self._DATE_FORMATS:
283 fmt = locale.get("custom.date_formats.{}".format(token))
284 if fmt is None:
285 fmt = self._DEFAULT_DATE_FORMATS[token]
287 return self.format(dt, fmt, locale)
289 if token in self._LOCALIZABLE_TOKENS:
290 return self._format_localizable_token(dt, token, locale)
292 if token in self._TOKENS_RULES:
293 return self._TOKENS_RULES[token](dt)
295 # Timezone
296 if token in ["ZZ", "Z"]:
297 if dt.tzinfo is None:
298 return ""
300 separator = ":" if token == "Z" else ""
301 offset = dt.utcoffset() or datetime.timedelta()
302 minutes = offset.total_seconds() / 60
304 if minutes >= 0:
305 sign = "+"
306 else:
307 sign = "-"
309 hour, minute = divmod(abs(int(minutes)), 60)
311 return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute)
313 def _format_localizable_token(
314 self, dt, token, locale
315 ): # type: (pendulum.DateTime, str, Locale) -> str
316 """
317 Formats a DateTime instance
318 with a given localizable token and locale.
320 :param dt: The instance to format
321 :type dt: pendulum.DateTime
323 :param token: The token to use
324 :type token: str
326 :param locale: The locale to use
327 :type locale: Locale
329 :rtype: str
330 """
331 if token == "MMM":
332 return locale.get("translations.months.abbreviated")[dt.month]
333 elif token == "MMMM":
334 return locale.get("translations.months.wide")[dt.month]
335 elif token == "dd":
336 return locale.get("translations.days.short")[dt.day_of_week]
337 elif token == "ddd":
338 return locale.get("translations.days.abbreviated")[dt.day_of_week]
339 elif token == "dddd":
340 return locale.get("translations.days.wide")[dt.day_of_week]
341 elif token == "Do":
342 return locale.ordinalize(dt.day)
343 elif token == "do":
344 return locale.ordinalize(dt.day_of_week)
345 elif token == "Mo":
346 return locale.ordinalize(dt.month)
347 elif token == "Qo":
348 return locale.ordinalize(dt.quarter)
349 elif token == "wo":
350 return locale.ordinalize(dt.week_of_year)
351 elif token == "DDDo":
352 return locale.ordinalize(dt.day_of_year)
353 elif token == "A":
354 key = "translations.day_periods"
355 if dt.hour >= 12:
356 key += ".pm"
357 else:
358 key += ".am"
360 return locale.get(key)
361 else:
362 return token
364 def parse(
365 self,
366 time, # type: str
367 fmt, # type: str
368 now, # type: pendulum.DateTime
369 locale=None, # type: typing.Optional[str]
370 ): # type: (...) -> typing.Dict[str, typing.Any]
371 """
372 Parses a time string matching a given format as a tuple.
374 :param time: The timestring
375 :param fmt: The format
376 :param now: The datetime to use as "now"
377 :param locale: The locale to use
379 :return: The parsed elements
380 """
381 escaped_fmt = re.escape(fmt)
383 tokens = self._FROM_FORMAT_RE.findall(escaped_fmt)
384 if not tokens:
385 return time
387 if not locale:
388 locale = pendulum.get_locale()
390 locale = Locale.load(locale)
392 parsed = {
393 "year": None,
394 "month": None,
395 "day": None,
396 "hour": None,
397 "minute": None,
398 "second": None,
399 "microsecond": None,
400 "tz": None,
401 "quarter": None,
402 "day_of_week": None,
403 "day_of_year": None,
404 "meridiem": None,
405 "timestamp": None,
406 }
408 pattern = self._FROM_FORMAT_RE.sub(
409 lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt
410 )
412 if not re.search("^" + pattern + "$", time):
413 raise ValueError("String does not match format {}".format(fmt))
415 re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time)
417 return self._check_parsed(parsed, now)
419 def _check_parsed(
420 self, parsed, now
421 ): # type: (typing.Dict[str, typing.Any], pendulum.DateTime) -> typing.Dict[str, typing.Any]
422 """
423 Checks validity of parsed elements.
425 :param parsed: The elements to parse.
427 :return: The validated elements.
428 """
429 validated = {
430 "year": parsed["year"],
431 "month": parsed["month"],
432 "day": parsed["day"],
433 "hour": parsed["hour"],
434 "minute": parsed["minute"],
435 "second": parsed["second"],
436 "microsecond": parsed["microsecond"],
437 "tz": None,
438 }
440 # If timestamp has been specified
441 # we use it and don't go any further
442 if parsed["timestamp"] is not None:
443 str_us = str(parsed["timestamp"])
444 if "." in str_us:
445 microseconds = int("{}".format(str_us.split(".")[1].ljust(6, "0")))
446 else:
447 microseconds = 0
449 from pendulum.helpers import local_time
451 time = local_time(parsed["timestamp"], 0, microseconds)
452 validated["year"] = time[0]
453 validated["month"] = time[1]
454 validated["day"] = time[2]
455 validated["hour"] = time[3]
456 validated["minute"] = time[4]
457 validated["second"] = time[5]
458 validated["microsecond"] = time[6]
460 return validated
462 if parsed["quarter"] is not None:
463 if validated["year"] is not None:
464 dt = pendulum.datetime(validated["year"], 1, 1)
465 else:
466 dt = now
468 dt = dt.start_of("year")
470 while dt.quarter != parsed["quarter"]:
471 dt = dt.add(months=3)
473 validated["year"] = dt.year
474 validated["month"] = dt.month
475 validated["day"] = dt.day
477 if validated["year"] is None:
478 validated["year"] = now.year
480 if parsed["day_of_year"] is not None:
481 dt = pendulum.parse(
482 "{}-{:>03d}".format(validated["year"], parsed["day_of_year"])
483 )
485 validated["month"] = dt.month
486 validated["day"] = dt.day
488 if parsed["day_of_week"] is not None:
489 dt = pendulum.datetime(
490 validated["year"],
491 validated["month"] or now.month,
492 validated["day"] or now.day,
493 )
494 dt = dt.start_of("week").subtract(days=1)
495 dt = dt.next(parsed["day_of_week"])
496 validated["year"] = dt.year
497 validated["month"] = dt.month
498 validated["day"] = dt.day
500 # Meridiem
501 if parsed["meridiem"] is not None:
502 # If the time is greater than 13:00:00
503 # This is not valid
504 if validated["hour"] is None:
505 raise ValueError("Invalid Date")
507 t = (
508 validated["hour"],
509 validated["minute"],
510 validated["second"],
511 validated["microsecond"],
512 )
513 if t >= (13, 0, 0, 0):
514 raise ValueError("Invalid date")
516 pm = parsed["meridiem"] == "pm"
517 validated["hour"] %= 12
518 if pm:
519 validated["hour"] += 12
521 if validated["month"] is None:
522 if parsed["year"] is not None:
523 validated["month"] = parsed["month"] or 1
524 else:
525 validated["month"] = parsed["month"] or now.month
527 if validated["day"] is None:
528 if parsed["year"] is not None or parsed["month"] is not None:
529 validated["day"] = parsed["day"] or 1
530 else:
531 validated["day"] = parsed["day"] or now.day
533 for part in ["hour", "minute", "second", "microsecond"]:
534 if validated[part] is None:
535 validated[part] = 0
537 validated["tz"] = parsed["tz"]
539 return validated
541 def _get_parsed_values(
542 self, m, parsed, locale, now
543 ): # type: (typing.Match[str], typing.Dict[str, typing.Any], Locale, pendulum.DateTime) -> None
544 for token, index in m.re.groupindex.items():
545 if token in self._LOCALIZABLE_TOKENS:
546 self._get_parsed_locale_value(token, m.group(index), parsed, locale)
547 else:
548 self._get_parsed_value(token, m.group(index), parsed, now)
550 def _get_parsed_value(
551 self, token, value, parsed, now
552 ): # type: (str, str, typing.Dict[str, typing.Any], pendulum.DateTime) -> None
553 parsed_token = self._PARSE_TOKENS[token](value)
555 if "Y" in token:
556 if token == "YY":
557 parsed_token = now.year // 100 * 100 + parsed_token
559 parsed["year"] = parsed_token
560 elif "Q" == token:
561 parsed["quarter"] = parsed_token
562 elif token in ["MM", "M"]:
563 parsed["month"] = parsed_token
564 elif token in ["DDDD", "DDD"]:
565 parsed["day_of_year"] = parsed_token
566 elif "D" in token:
567 parsed["day"] = parsed_token
568 elif "H" in token:
569 parsed["hour"] = parsed_token
570 elif token in ["hh", "h"]:
571 if parsed_token > 12:
572 raise ValueError("Invalid date")
574 parsed["hour"] = parsed_token
575 elif "m" in token:
576 parsed["minute"] = parsed_token
577 elif "s" in token:
578 parsed["second"] = parsed_token
579 elif "S" in token:
580 parsed["microsecond"] = parsed_token
581 elif token in ["d", "E"]:
582 parsed["day_of_week"] = parsed_token
583 elif token in ["X", "x"]:
584 parsed["timestamp"] = parsed_token
585 elif token in ["ZZ", "Z"]:
586 negative = True if value.startswith("-") else False
587 tz = value[1:]
588 if ":" not in tz:
589 if len(tz) == 2:
590 tz = "{}00".format(tz)
592 off_hour = tz[0:2]
593 off_minute = tz[2:4]
594 else:
595 off_hour, off_minute = tz.split(":")
597 offset = ((int(off_hour) * 60) + int(off_minute)) * 60
599 if negative:
600 offset = -1 * offset
602 parsed["tz"] = pendulum.timezone(offset)
603 elif token == "z":
604 # Full timezone
605 if value not in pendulum.timezones:
606 raise ValueError("Invalid date")
608 parsed["tz"] = pendulum.timezone(value)
610 def _get_parsed_locale_value(
611 self, token, value, parsed, locale
612 ): # type: (str, str, typing.Dict[str, typing.Any], Locale) -> None
613 if token == "MMMM":
614 unit = "month"
615 match = "months.wide"
616 elif token == "MMM":
617 unit = "month"
618 match = "months.abbreviated"
619 elif token == "Do":
620 parsed["day"] = int(re.match(r"(\d+)", value).group(1))
622 return
623 elif token == "dddd":
624 unit = "day_of_week"
625 match = "days.wide"
626 elif token == "ddd":
627 unit = "day_of_week"
628 match = "days.abbreviated"
629 elif token == "dd":
630 unit = "day_of_week"
631 match = "days.short"
632 elif token in ["a", "A"]:
633 valid_values = [
634 locale.translation("day_periods.am"),
635 locale.translation("day_periods.pm"),
636 ]
638 if token == "a":
639 value = value.lower()
640 valid_values = list(map(lambda x: x.lower(), valid_values))
642 if value not in valid_values:
643 raise ValueError("Invalid date")
645 parsed["meridiem"] = ["am", "pm"][valid_values.index(value)]
647 return
648 else:
649 raise ValueError('Invalid token "{}"'.format(token))
651 parsed[unit] = locale.match_translation(match, value)
652 if value is None:
653 raise ValueError("Invalid date")
655 def _replace_tokens(self, token, locale): # type: (str, Locale) -> str
656 if token.startswith("[") and token.endswith("]"):
657 return token[1:-1]
658 elif token.startswith("\\"):
659 if len(token) == 2 and token[1] in {"[", "]"}:
660 return ""
662 return token
663 elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS:
664 raise ValueError("Unsupported token: {}".format(token))
666 if token in self._LOCALIZABLE_TOKENS:
667 values = self._LOCALIZABLE_TOKENS[token]
668 if callable(values):
669 candidates = values(locale)
670 else:
671 candidates = tuple(
672 locale.translation(self._LOCALIZABLE_TOKENS[token]).values()
673 )
674 else:
675 candidates = self._REGEX_TOKENS[token]
677 if not candidates:
678 raise ValueError("Unsupported token: {}".format(token))
680 if not isinstance(candidates, tuple):
681 candidates = (candidates,)
683 pattern = "(?P<{}>{})".format(token, "|".join([decode(p) for p in candidates]))
685 return pattern