formatter.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """Provides the :class:`Arrow <arrow.formatter.DateTimeFormatter>` class, an improved formatter for datetimes."""
  2. import re
  3. from datetime import datetime, timedelta, timezone
  4. from typing import Final, Optional, Pattern, cast
  5. from arrow import locales
  6. from arrow.constants import DEFAULT_LOCALE
  7. FORMAT_ATOM: Final[str] = "YYYY-MM-DD HH:mm:ssZZ"
  8. FORMAT_COOKIE: Final[str] = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ"
  9. FORMAT_RFC822: Final[str] = "ddd, DD MMM YY HH:mm:ss Z"
  10. FORMAT_RFC850: Final[str] = "dddd, DD-MMM-YY HH:mm:ss ZZZ"
  11. FORMAT_RFC1036: Final[str] = "ddd, DD MMM YY HH:mm:ss Z"
  12. FORMAT_RFC1123: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z"
  13. FORMAT_RFC2822: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z"
  14. FORMAT_RFC3339: Final[str] = "YYYY-MM-DD HH:mm:ssZZ"
  15. FORMAT_RFC3339_STRICT: Final[str] = "YYYY-MM-DDTHH:mm:ssZZ"
  16. FORMAT_RSS: Final[str] = "ddd, DD MMM YYYY HH:mm:ss Z"
  17. FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ"
  18. class DateTimeFormatter:
  19. # This pattern matches characters enclosed in square brackets are matched as
  20. # an atomic group. For more info on atomic groups and how to they are
  21. # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578
  22. _FORMAT_RE: Final[Pattern[str]] = re.compile(
  23. r"(\[(?:(?=(?P<literal>[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)"
  24. )
  25. locale: locales.Locale
  26. def __init__(self, locale: str = DEFAULT_LOCALE) -> None:
  27. self.locale = locales.get_locale(locale)
  28. def format(cls, dt: datetime, fmt: str) -> str:
  29. # FIXME: _format_token() is nullable
  30. return cls._FORMAT_RE.sub(
  31. lambda m: cast(str, cls._format_token(dt, m.group(0))), fmt
  32. )
  33. def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]:
  34. if token and token.startswith("[") and token.endswith("]"):
  35. return token[1:-1]
  36. if token == "YYYY":
  37. return self.locale.year_full(dt.year)
  38. if token == "YY":
  39. return self.locale.year_abbreviation(dt.year)
  40. if token == "MMMM":
  41. return self.locale.month_name(dt.month)
  42. if token == "MMM":
  43. return self.locale.month_abbreviation(dt.month)
  44. if token == "MM":
  45. return f"{dt.month:02d}"
  46. if token == "M":
  47. return f"{dt.month}"
  48. if token == "DDDD":
  49. return f"{dt.timetuple().tm_yday:03d}"
  50. if token == "DDD":
  51. return f"{dt.timetuple().tm_yday}"
  52. if token == "DD":
  53. return f"{dt.day:02d}"
  54. if token == "D":
  55. return f"{dt.day}"
  56. if token == "Do":
  57. return self.locale.ordinal_number(dt.day)
  58. if token == "dddd":
  59. return self.locale.day_name(dt.isoweekday())
  60. if token == "ddd":
  61. return self.locale.day_abbreviation(dt.isoweekday())
  62. if token == "d":
  63. return f"{dt.isoweekday()}"
  64. if token == "HH":
  65. return f"{dt.hour:02d}"
  66. if token == "H":
  67. return f"{dt.hour}"
  68. if token == "hh":
  69. return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12):02d}"
  70. if token == "h":
  71. return f"{dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)}"
  72. if token == "mm":
  73. return f"{dt.minute:02d}"
  74. if token == "m":
  75. return f"{dt.minute}"
  76. if token == "ss":
  77. return f"{dt.second:02d}"
  78. if token == "s":
  79. return f"{dt.second}"
  80. if token == "SSSSSS":
  81. return f"{dt.microsecond:06d}"
  82. if token == "SSSSS":
  83. return f"{dt.microsecond // 10:05d}"
  84. if token == "SSSS":
  85. return f"{dt.microsecond // 100:04d}"
  86. if token == "SSS":
  87. return f"{dt.microsecond // 1000:03d}"
  88. if token == "SS":
  89. return f"{dt.microsecond // 10000:02d}"
  90. if token == "S":
  91. return f"{dt.microsecond // 100000}"
  92. if token == "X":
  93. return f"{dt.timestamp()}"
  94. if token == "x":
  95. return f"{dt.timestamp() * 1_000_000:.0f}"
  96. if token == "ZZZ":
  97. return dt.tzname()
  98. if token in ["ZZ", "Z"]:
  99. separator = ":" if token == "ZZ" else ""
  100. tz = timezone.utc if dt.tzinfo is None else dt.tzinfo
  101. # `dt` must be aware object. Otherwise, this line will raise AttributeError
  102. # https://github.com/arrow-py/arrow/pull/883#discussion_r529866834
  103. # datetime awareness: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects
  104. total_minutes = int(cast(timedelta, tz.utcoffset(dt)).total_seconds() / 60)
  105. sign = "+" if total_minutes >= 0 else "-"
  106. total_minutes = abs(total_minutes)
  107. hour, minute = divmod(total_minutes, 60)
  108. return f"{sign}{hour:02d}{separator}{minute:02d}"
  109. if token in ("a", "A"):
  110. return self.locale.meridian(dt.hour, token)
  111. if token == "W":
  112. year, week, day = dt.isocalendar()
  113. return f"{year}-W{week:02d}-{day}"