OpenVoiceOS/buildroot-external/board/raspberrypi/rootfs-overlay/usr/lib/python3.10/site-packages/pendulum/formatting/formatter.py

686 lines
22 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import re
import typing
import pendulum
from pendulum.locales.locale import Locale
from pendulum.utils._compat import decode
_MATCH_1 = r"\d"
_MATCH_2 = r"\d\d"
_MATCH_3 = r"\d{3}"
_MATCH_4 = r"\d{4}"
_MATCH_6 = r"[+-]?\d{6}"
_MATCH_1_TO_2 = r"\d\d?"
_MATCH_1_TO_2_LEFT_PAD = r"[0-9 ]\d?"
_MATCH_1_TO_3 = r"\d{1,3}"
_MATCH_1_TO_4 = r"\d{1,4}"
_MATCH_1_TO_6 = r"[+-]?\d{1,6}"
_MATCH_3_TO_4 = r"\d{3}\d?"
_MATCH_5_TO_6 = r"\d{5}\d?"
_MATCH_UNSIGNED = r"\d+"
_MATCH_SIGNED = r"[+-]?\d+"
_MATCH_OFFSET = r"[Zz]|[+-]\d\d:?\d\d"
_MATCH_SHORT_OFFSET = r"[Zz]|[+-]\d\d(?::?\d\d)?"
_MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?"
_MATCH_WORD = (
"(?i)[0-9]*"
"['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+"
r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}"
)
_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?"
class Formatter:
_TOKENS = (
r"\[([^\[]*)\]|\\(.)|"
"("
"Mo|MM?M?M?"
"|Do|DDDo|DD?D?D?|ddd?d?|do?"
"|E{1,4}"
"|w[o|w]?|W[o|W]?|Qo?"
"|YYYY|YY|Y"
"|gg(ggg?)?|GG(GGG?)?"
"|a|A"
"|hh?|HH?|kk?"
"|mm?|ss?|S{1,9}"
"|x|X"
"|zz?|ZZ?"
"|LTS|LT|LL?L?L?"
")"
)
_FORMAT_RE = re.compile(_TOKENS)
_FROM_FORMAT_RE = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])")
_LOCALIZABLE_TOKENS = {
"Qo": None,
"MMMM": "months.wide",
"MMM": "months.abbreviated",
"Mo": None,
"DDDo": None,
"Do": lambda locale: tuple(
r"\d+{}".format(o) for o in locale.get("custom.ordinal").values()
),
"dddd": "days.wide",
"ddd": "days.abbreviated",
"dd": "days.short",
"do": None,
"Wo": None,
"wo": None,
"A": lambda locale: (
locale.translation("day_periods.am"),
locale.translation("day_periods.pm"),
),
"a": lambda locale: (
locale.translation("day_periods.am").lower(),
locale.translation("day_periods.pm").lower(),
),
}
_TOKENS_RULES = {
# Year
"YYYY": lambda dt: "{:d}".format(dt.year),
"YY": lambda dt: "{:d}".format(dt.year)[2:],
"Y": lambda dt: "{:d}".format(dt.year),
# Quarter
"Q": lambda dt: "{:d}".format(dt.quarter),
# Month
"MM": lambda dt: "{:02d}".format(dt.month),
"M": lambda dt: "{:d}".format(dt.month),
# Day
"DD": lambda dt: "{:02d}".format(dt.day),
"D": lambda dt: "{:d}".format(dt.day),
# Day of Year
"DDDD": lambda dt: "{:03d}".format(dt.day_of_year),
"DDD": lambda dt: "{:d}".format(dt.day_of_year),
# Day of Week
"d": lambda dt: "{:d}".format(dt.day_of_week),
# Day of ISO Week
"E": lambda dt: "{:d}".format(dt.isoweekday()),
# Hour
"HH": lambda dt: "{:02d}".format(dt.hour),
"H": lambda dt: "{:d}".format(dt.hour),
"hh": lambda dt: "{:02d}".format(dt.hour % 12 or 12),
"h": lambda dt: "{:d}".format(dt.hour % 12 or 12),
# Minute
"mm": lambda dt: "{:02d}".format(dt.minute),
"m": lambda dt: "{:d}".format(dt.minute),
# Second
"ss": lambda dt: "{:02d}".format(dt.second),
"s": lambda dt: "{:d}".format(dt.second),
# Fractional second
"S": lambda dt: "{:01d}".format(dt.microsecond // 100000),
"SS": lambda dt: "{:02d}".format(dt.microsecond // 10000),
"SSS": lambda dt: "{:03d}".format(dt.microsecond // 1000),
"SSSS": lambda dt: "{:04d}".format(dt.microsecond // 100),
"SSSSS": lambda dt: "{:05d}".format(dt.microsecond // 10),
"SSSSSS": lambda dt: "{:06d}".format(dt.microsecond),
# Timestamp
"X": lambda dt: "{:d}".format(dt.int_timestamp),
"x": lambda dt: "{:d}".format(dt.int_timestamp * 1000 + dt.microsecond // 1000),
# Timezone
"zz": lambda dt: "{}".format(dt.tzname() if dt.tzinfo is not None else ""),
"z": lambda dt: "{}".format(dt.timezone_name or ""),
}
_DATE_FORMATS = {
"LTS": "formats.time.full",
"LT": "formats.time.short",
"L": "formats.date.short",
"LL": "formats.date.long",
"LLL": "formats.datetime.long",
"LLLL": "formats.datetime.full",
}
_DEFAULT_DATE_FORMATS = {
"LTS": "h:mm:ss A",
"LT": "h:mm A",
"L": "MM/DD/YYYY",
"LL": "MMMM D, YYYY",
"LLL": "MMMM D, YYYY h:mm A",
"LLLL": "dddd, MMMM D, YYYY h:mm A",
}
_REGEX_TOKENS = {
"Y": _MATCH_SIGNED,
"YY": (_MATCH_1_TO_2, _MATCH_2),
"YYYY": (_MATCH_1_TO_4, _MATCH_4),
"Q": _MATCH_1,
"Qo": None,
"M": _MATCH_1_TO_2,
"MM": (_MATCH_1_TO_2, _MATCH_2),
"MMM": _MATCH_WORD,
"MMMM": _MATCH_WORD,
"D": _MATCH_1_TO_2,
"DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2),
"DDD": _MATCH_1_TO_3,
"DDDD": _MATCH_3,
"dddd": _MATCH_WORD,
"ddd": _MATCH_WORD,
"dd": _MATCH_WORD,
"d": _MATCH_1,
"E": _MATCH_1,
"Do": None,
"H": _MATCH_1_TO_2,
"HH": (_MATCH_1_TO_2, _MATCH_2),
"h": _MATCH_1_TO_2,
"hh": (_MATCH_1_TO_2, _MATCH_2),
"m": _MATCH_1_TO_2,
"mm": (_MATCH_1_TO_2, _MATCH_2),
"s": _MATCH_1_TO_2,
"ss": (_MATCH_1_TO_2, _MATCH_2),
"S": (_MATCH_1_TO_3, _MATCH_1),
"SS": (_MATCH_1_TO_3, _MATCH_2),
"SSS": (_MATCH_1_TO_3, _MATCH_3),
"SSSS": _MATCH_UNSIGNED,
"SSSSS": _MATCH_UNSIGNED,
"SSSSSS": _MATCH_UNSIGNED,
"x": _MATCH_SIGNED,
"X": _MATCH_TIMESTAMP,
"ZZ": _MATCH_SHORT_OFFSET,
"Z": _MATCH_OFFSET,
"z": _MATCH_TIMEZONE,
}
_PARSE_TOKENS = {
"YYYY": lambda year: int(year),
"YY": lambda year: int(year),
"Q": lambda quarter: int(quarter),
"MMMM": lambda month: month,
"MMM": lambda month: month,
"MM": lambda month: int(month),
"M": lambda month: int(month),
"DDDD": lambda day: int(day),
"DDD": lambda day: int(day),
"DD": lambda day: int(day),
"D": lambda day: int(day),
"dddd": lambda weekday: weekday,
"ddd": lambda weekday: weekday,
"dd": lambda weekday: weekday,
"d": lambda weekday: int(weekday) % 7,
"E": lambda weekday: int(weekday),
"HH": lambda hour: int(hour),
"H": lambda hour: int(hour),
"hh": lambda hour: int(hour),
"h": lambda hour: int(hour),
"mm": lambda minute: int(minute),
"m": lambda minute: int(minute),
"ss": lambda second: int(second),
"s": lambda second: int(second),
"S": lambda us: int(us) * 100000,
"SS": lambda us: int(us) * 10000,
"SSS": lambda us: int(us) * 1000,
"SSSS": lambda us: int(us) * 100,
"SSSSS": lambda us: int(us) * 10,
"SSSSSS": lambda us: int(us),
"a": lambda meridiem: meridiem,
"X": lambda ts: float(ts),
"x": lambda ts: float(ts) / 1e3,
"ZZ": str,
"Z": str,
"z": str,
}
def format(
self, dt, fmt, locale=None
): # type: (pendulum.DateTime, str, typing.Optional[typing.Union[str, Locale]]) -> str
"""
Formats a DateTime instance with a given format and locale.
:param dt: The instance to format
:type dt: pendulum.DateTime
:param fmt: The format to use
:type fmt: str
:param locale: The locale to use
:type locale: str or Locale or None
:rtype: str
"""
if not locale:
locale = pendulum.get_locale()
locale = Locale.load(locale)
result = self._FORMAT_RE.sub(
lambda m: m.group(1)
if m.group(1)
else m.group(2)
if m.group(2)
else self._format_token(dt, m.group(3), locale),
fmt,
)
return decode(result)
def _format_token(
self, dt, token, locale
): # type: (pendulum.DateTime, str, Locale) -> str
"""
Formats a DateTime instance with a given token and locale.
:param dt: The instance to format
:type dt: pendulum.DateTime
:param token: The token to use
:type token: str
:param locale: The locale to use
:type locale: Locale
:rtype: str
"""
if token in self._DATE_FORMATS:
fmt = locale.get("custom.date_formats.{}".format(token))
if fmt is None:
fmt = self._DEFAULT_DATE_FORMATS[token]
return self.format(dt, fmt, locale)
if token in self._LOCALIZABLE_TOKENS:
return self._format_localizable_token(dt, token, locale)
if token in self._TOKENS_RULES:
return self._TOKENS_RULES[token](dt)
# Timezone
if token in ["ZZ", "Z"]:
if dt.tzinfo is None:
return ""
separator = ":" if token == "Z" else ""
offset = dt.utcoffset() or datetime.timedelta()
minutes = offset.total_seconds() / 60
if minutes >= 0:
sign = "+"
else:
sign = "-"
hour, minute = divmod(abs(int(minutes)), 60)
return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute)
def _format_localizable_token(
self, dt, token, locale
): # type: (pendulum.DateTime, str, Locale) -> str
"""
Formats a DateTime instance
with a given localizable token and locale.
:param dt: The instance to format
:type dt: pendulum.DateTime
:param token: The token to use
:type token: str
:param locale: The locale to use
:type locale: Locale
:rtype: str
"""
if token == "MMM":
return locale.get("translations.months.abbreviated")[dt.month]
elif token == "MMMM":
return locale.get("translations.months.wide")[dt.month]
elif token == "dd":
return locale.get("translations.days.short")[dt.day_of_week]
elif token == "ddd":
return locale.get("translations.days.abbreviated")[dt.day_of_week]
elif token == "dddd":
return locale.get("translations.days.wide")[dt.day_of_week]
elif token == "Do":
return locale.ordinalize(dt.day)
elif token == "do":
return locale.ordinalize(dt.day_of_week)
elif token == "Mo":
return locale.ordinalize(dt.month)
elif token == "Qo":
return locale.ordinalize(dt.quarter)
elif token == "wo":
return locale.ordinalize(dt.week_of_year)
elif token == "DDDo":
return locale.ordinalize(dt.day_of_year)
elif token == "A":
key = "translations.day_periods"
if dt.hour >= 12:
key += ".pm"
else:
key += ".am"
return locale.get(key)
else:
return token
def parse(
self,
time, # type: str
fmt, # type: str
now, # type: pendulum.DateTime
locale=None, # type: typing.Optional[str]
): # type: (...) -> typing.Dict[str, typing.Any]
"""
Parses a time string matching a given format as a tuple.
:param time: The timestring
:param fmt: The format
:param now: The datetime to use as "now"
:param locale: The locale to use
:return: The parsed elements
"""
escaped_fmt = re.escape(fmt)
tokens = self._FROM_FORMAT_RE.findall(escaped_fmt)
if not tokens:
return time
if not locale:
locale = pendulum.get_locale()
locale = Locale.load(locale)
parsed = {
"year": None,
"month": None,
"day": None,
"hour": None,
"minute": None,
"second": None,
"microsecond": None,
"tz": None,
"quarter": None,
"day_of_week": None,
"day_of_year": None,
"meridiem": None,
"timestamp": None,
}
pattern = self._FROM_FORMAT_RE.sub(
lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt
)
if not re.search("^" + pattern + "$", time):
raise ValueError("String does not match format {}".format(fmt))
re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time)
return self._check_parsed(parsed, now)
def _check_parsed(
self, parsed, now
): # type: (typing.Dict[str, typing.Any], pendulum.DateTime) -> typing.Dict[str, typing.Any]
"""
Checks validity of parsed elements.
:param parsed: The elements to parse.
:return: The validated elements.
"""
validated = {
"year": parsed["year"],
"month": parsed["month"],
"day": parsed["day"],
"hour": parsed["hour"],
"minute": parsed["minute"],
"second": parsed["second"],
"microsecond": parsed["microsecond"],
"tz": None,
}
# If timestamp has been specified
# we use it and don't go any further
if parsed["timestamp"] is not None:
str_us = str(parsed["timestamp"])
if "." in str_us:
microseconds = int("{}".format(str_us.split(".")[1].ljust(6, "0")))
else:
microseconds = 0
from pendulum.helpers import local_time
time = local_time(parsed["timestamp"], 0, microseconds)
validated["year"] = time[0]
validated["month"] = time[1]
validated["day"] = time[2]
validated["hour"] = time[3]
validated["minute"] = time[4]
validated["second"] = time[5]
validated["microsecond"] = time[6]
return validated
if parsed["quarter"] is not None:
if validated["year"] is not None:
dt = pendulum.datetime(validated["year"], 1, 1)
else:
dt = now
dt = dt.start_of("year")
while dt.quarter != parsed["quarter"]:
dt = dt.add(months=3)
validated["year"] = dt.year
validated["month"] = dt.month
validated["day"] = dt.day
if validated["year"] is None:
validated["year"] = now.year
if parsed["day_of_year"] is not None:
dt = pendulum.parse(
"{}-{:>03d}".format(validated["year"], parsed["day_of_year"])
)
validated["month"] = dt.month
validated["day"] = dt.day
if parsed["day_of_week"] is not None:
dt = pendulum.datetime(
validated["year"],
validated["month"] or now.month,
validated["day"] or now.day,
)
dt = dt.start_of("week").subtract(days=1)
dt = dt.next(parsed["day_of_week"])
validated["year"] = dt.year
validated["month"] = dt.month
validated["day"] = dt.day
# Meridiem
if parsed["meridiem"] is not None:
# If the time is greater than 13:00:00
# This is not valid
if validated["hour"] is None:
raise ValueError("Invalid Date")
t = (
validated["hour"],
validated["minute"],
validated["second"],
validated["microsecond"],
)
if t >= (13, 0, 0, 0):
raise ValueError("Invalid date")
pm = parsed["meridiem"] == "pm"
validated["hour"] %= 12
if pm:
validated["hour"] += 12
if validated["month"] is None:
if parsed["year"] is not None:
validated["month"] = parsed["month"] or 1
else:
validated["month"] = parsed["month"] or now.month
if validated["day"] is None:
if parsed["year"] is not None or parsed["month"] is not None:
validated["day"] = parsed["day"] or 1
else:
validated["day"] = parsed["day"] or now.day
for part in ["hour", "minute", "second", "microsecond"]:
if validated[part] is None:
validated[part] = 0
validated["tz"] = parsed["tz"]
return validated
def _get_parsed_values(
self, m, parsed, locale, now
): # type: (typing.Match[str], typing.Dict[str, typing.Any], Locale, pendulum.DateTime) -> None
for token, index in m.re.groupindex.items():
if token in self._LOCALIZABLE_TOKENS:
self._get_parsed_locale_value(token, m.group(index), parsed, locale)
else:
self._get_parsed_value(token, m.group(index), parsed, now)
def _get_parsed_value(
self, token, value, parsed, now
): # type: (str, str, typing.Dict[str, typing.Any], pendulum.DateTime) -> None
parsed_token = self._PARSE_TOKENS[token](value)
if "Y" in token:
if token == "YY":
parsed_token = now.year // 100 * 100 + parsed_token
parsed["year"] = parsed_token
elif "Q" == token:
parsed["quarter"] = parsed_token
elif token in ["MM", "M"]:
parsed["month"] = parsed_token
elif token in ["DDDD", "DDD"]:
parsed["day_of_year"] = parsed_token
elif "D" in token:
parsed["day"] = parsed_token
elif "H" in token:
parsed["hour"] = parsed_token
elif token in ["hh", "h"]:
if parsed_token > 12:
raise ValueError("Invalid date")
parsed["hour"] = parsed_token
elif "m" in token:
parsed["minute"] = parsed_token
elif "s" in token:
parsed["second"] = parsed_token
elif "S" in token:
parsed["microsecond"] = parsed_token
elif token in ["d", "E"]:
parsed["day_of_week"] = parsed_token
elif token in ["X", "x"]:
parsed["timestamp"] = parsed_token
elif token in ["ZZ", "Z"]:
negative = True if value.startswith("-") else False
tz = value[1:]
if ":" not in tz:
if len(tz) == 2:
tz = "{}00".format(tz)
off_hour = tz[0:2]
off_minute = tz[2:4]
else:
off_hour, off_minute = tz.split(":")
offset = ((int(off_hour) * 60) + int(off_minute)) * 60
if negative:
offset = -1 * offset
parsed["tz"] = pendulum.timezone(offset)
elif token == "z":
# Full timezone
if value not in pendulum.timezones:
raise ValueError("Invalid date")
parsed["tz"] = pendulum.timezone(value)
def _get_parsed_locale_value(
self, token, value, parsed, locale
): # type: (str, str, typing.Dict[str, typing.Any], Locale) -> None
if token == "MMMM":
unit = "month"
match = "months.wide"
elif token == "MMM":
unit = "month"
match = "months.abbreviated"
elif token == "Do":
parsed["day"] = int(re.match(r"(\d+)", value).group(1))
return
elif token == "dddd":
unit = "day_of_week"
match = "days.wide"
elif token == "ddd":
unit = "day_of_week"
match = "days.abbreviated"
elif token == "dd":
unit = "day_of_week"
match = "days.short"
elif token in ["a", "A"]:
valid_values = [
locale.translation("day_periods.am"),
locale.translation("day_periods.pm"),
]
if token == "a":
value = value.lower()
valid_values = list(map(lambda x: x.lower(), valid_values))
if value not in valid_values:
raise ValueError("Invalid date")
parsed["meridiem"] = ["am", "pm"][valid_values.index(value)]
return
else:
raise ValueError('Invalid token "{}"'.format(token))
parsed[unit] = locale.match_translation(match, value)
if value is None:
raise ValueError("Invalid date")
def _replace_tokens(self, token, locale): # type: (str, Locale) -> str
if token.startswith("[") and token.endswith("]"):
return token[1:-1]
elif token.startswith("\\"):
if len(token) == 2 and token[1] in {"[", "]"}:
return ""
return token
elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS:
raise ValueError("Unsupported token: {}".format(token))
if token in self._LOCALIZABLE_TOKENS:
values = self._LOCALIZABLE_TOKENS[token]
if callable(values):
candidates = values(locale)
else:
candidates = tuple(
locale.translation(self._LOCALIZABLE_TOKENS[token]).values()
)
else:
candidates = self._REGEX_TOKENS[token]
if not candidates:
raise ValueError("Unsupported token: {}".format(token))
if not isinstance(candidates, tuple):
candidates = (candidates,)
pattern = "(?P<{}>{})".format(token, "|".join([decode(p) for p in candidates]))
return pattern