Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/dates.py: 12%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2babel.dates
3~~~~~~~~~~~
5Locale dependent formatting and parsing of dates and times.
7The default locale for the functions in this module is determined by the
8following environment variables, in that order:
10 * ``LC_TIME``,
11 * ``LC_ALL``, and
12 * ``LANG``
14:copyright: (c) 2013-2026 by the Babel Team.
15:license: BSD, see LICENSE for more details.
16"""
18from __future__ import annotations
20import math
21import re
22import warnings
23from functools import lru_cache
24from typing import TYPE_CHECKING, Literal, SupportsInt
26try:
27 import pytz
28except ModuleNotFoundError:
29 pytz = None
30 import zoneinfo
32import datetime
33from collections.abc import Iterable
35from babel import localtime
36from babel.core import Locale, default_locale, get_global
37from babel.localedata import LocaleDataDict
39if TYPE_CHECKING:
40 from typing_extensions import TypeAlias
42 _Instant: TypeAlias = datetime.date | datetime.time | float | None
43 _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short']
44 _Context: TypeAlias = Literal['format', 'stand-alone']
45 _DtOrTzinfo: TypeAlias = datetime.datetime | datetime.tzinfo | str | int | datetime.time | None # fmt: skip
47# "If a given short metazone form is known NOT to be understood in a given
48# locale and the parent locale has this value such that it would normally
49# be inherited, the inheritance of this value can be explicitly disabled by
50# use of the 'no inheritance marker' as the value, which is 3 simultaneous [sic]
51# empty set characters ( U+2205 )."
52# - https://www.unicode.org/reports/tr35/tr35-dates.html#Metazone_Names
54NO_INHERITANCE_MARKER = '\u2205\u2205\u2205'
56UTC = datetime.timezone.utc
57LOCALTZ = localtime.LOCALTZ
59LC_TIME = default_locale('LC_TIME')
62def _localize(tz: datetime.tzinfo, dt: datetime.datetime) -> datetime.datetime:
63 # Support localizing with both pytz and zoneinfo tzinfos
64 # nothing to do
65 if dt.tzinfo is tz:
66 return dt
68 if hasattr(tz, 'localize'): # pytz
69 return tz.localize(dt)
71 if dt.tzinfo is None:
72 # convert naive to localized
73 return dt.replace(tzinfo=tz)
75 # convert timezones
76 return dt.astimezone(tz)
79def _get_dt_and_tzinfo(
80 dt_or_tzinfo: _DtOrTzinfo,
81) -> tuple[datetime.datetime | None, datetime.tzinfo]:
82 """
83 Parse a `dt_or_tzinfo` value into a datetime and a tzinfo.
85 See the docs for this function's callers for semantics.
87 :rtype: tuple[datetime, tzinfo]
88 """
89 if dt_or_tzinfo is None:
90 dt = datetime.datetime.now()
91 tzinfo = LOCALTZ
92 elif isinstance(dt_or_tzinfo, str):
93 dt = None
94 tzinfo = get_timezone(dt_or_tzinfo)
95 elif isinstance(dt_or_tzinfo, int):
96 dt = None
97 tzinfo = UTC
98 elif isinstance(dt_or_tzinfo, (datetime.datetime, datetime.time)):
99 dt = _get_datetime(dt_or_tzinfo)
100 tzinfo = dt.tzinfo if dt.tzinfo is not None else UTC
101 else:
102 dt = None
103 tzinfo = dt_or_tzinfo
104 return dt, tzinfo
107def _get_tz_name(dt_or_tzinfo: _DtOrTzinfo) -> str:
108 """
109 Get the timezone name out of a time, datetime, or tzinfo object.
111 :rtype: str
112 """
113 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
114 if hasattr(tzinfo, 'zone'): # pytz object
115 return tzinfo.zone
116 elif hasattr(tzinfo, 'key') and tzinfo.key is not None: # ZoneInfo object
117 return tzinfo.key
118 else:
119 return tzinfo.tzname(dt or datetime.datetime.now(UTC))
122def _get_datetime(instant: _Instant) -> datetime.datetime:
123 """
124 Get a datetime out of an "instant" (date, time, datetime, number).
126 .. warning:: The return values of this function may depend on the system clock.
128 If the instant is None, the current moment is used.
129 If the instant is a time, it's augmented with today's date.
131 Dates are converted to naive datetimes with midnight as the time component.
133 >>> from datetime import date, datetime
134 >>> _get_datetime(date(2015, 1, 1))
135 datetime.datetime(2015, 1, 1, 0, 0)
137 UNIX timestamps are converted to datetimes.
139 >>> _get_datetime(1400000000)
140 datetime.datetime(2014, 5, 13, 16, 53, 20)
142 Other values are passed through as-is.
144 >>> x = datetime(2015, 1, 1)
145 >>> _get_datetime(x) is x
146 True
148 :param instant: date, time, datetime, integer, float or None
149 :return: a datetime
150 """
151 if instant is None:
152 return datetime.datetime.now(UTC).replace(tzinfo=None)
153 elif isinstance(instant, (int, float)):
154 return datetime.datetime.fromtimestamp(instant, UTC).replace(tzinfo=None)
155 elif isinstance(instant, datetime.time):
156 return datetime.datetime.combine(datetime.date.today(), instant)
157 elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): # fmt: skip
158 return datetime.datetime.combine(instant, datetime.time())
159 # TODO (3.x): Add an assertion/type check for this fallthrough branch:
160 return instant
163def _ensure_datetime_tzinfo(
164 dt: datetime.datetime,
165 tzinfo: datetime.tzinfo | None = None,
166) -> datetime.datetime:
167 """
168 Ensure the datetime passed has an attached tzinfo.
170 If the datetime is tz-naive to begin with, UTC is attached.
172 If a tzinfo is passed in, the datetime is normalized to that timezone.
174 >>> from datetime import datetime
175 >>> _get_tz_name(_ensure_datetime_tzinfo(datetime(2015, 1, 1)))
176 'UTC'
178 >>> tz = get_timezone("Europe/Stockholm")
179 >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1, 13, 15, tzinfo=UTC), tzinfo=tz).hour
180 14
182 :param datetime: Datetime to augment.
183 :param tzinfo: optional tzinfo
184 :return: datetime with tzinfo
185 :rtype: datetime
186 """
187 if dt.tzinfo is None:
188 dt = dt.replace(tzinfo=UTC)
189 if tzinfo is not None:
190 dt = dt.astimezone(get_timezone(tzinfo))
191 if hasattr(tzinfo, 'normalize'): # pytz
192 dt = tzinfo.normalize(dt)
193 return dt
196def _get_time(
197 time: datetime.time | datetime.datetime | None,
198 tzinfo: datetime.tzinfo | None = None,
199) -> datetime.time:
200 """
201 Get a timezoned time from a given instant.
203 .. warning:: The return values of this function may depend on the system clock.
205 :param time: time, datetime or None
206 :rtype: time
207 """
208 if time is None:
209 time = datetime.datetime.now(UTC)
210 elif isinstance(time, (int, float)):
211 time = datetime.datetime.fromtimestamp(time, UTC)
213 if time.tzinfo is None:
214 time = time.replace(tzinfo=UTC)
216 if isinstance(time, datetime.datetime):
217 if tzinfo is not None:
218 time = time.astimezone(tzinfo)
219 if hasattr(tzinfo, 'normalize'): # pytz
220 time = tzinfo.normalize(time)
221 time = time.timetz()
222 elif tzinfo is not None:
223 time = time.replace(tzinfo=tzinfo)
224 return time
227def get_timezone(zone: str | datetime.tzinfo | None = None) -> datetime.tzinfo:
228 """Looks up a timezone by name and returns it. The timezone object
229 returned comes from ``pytz`` or ``zoneinfo``, whichever is available.
230 It corresponds to the `tzinfo` interface and can be used with all of
231 the functions of Babel that operate with dates.
233 If a timezone is not known a :exc:`LookupError` is raised. If `zone`
234 is ``None`` a local zone object is returned.
236 :param zone: the name of the timezone to look up. If a timezone object
237 itself is passed in, it's returned unchanged.
238 """
239 if zone is None:
240 return LOCALTZ
241 if not isinstance(zone, str):
242 return zone
244 if pytz:
245 try:
246 return pytz.timezone(zone)
247 except pytz.UnknownTimeZoneError as e:
248 exc = e
249 else:
250 assert zoneinfo
251 try:
252 return zoneinfo.ZoneInfo(zone)
253 except zoneinfo.ZoneInfoNotFoundError as e:
254 exc = e
256 raise LookupError(f"Unknown timezone {zone}") from exc
259def get_period_names(
260 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
261 context: _Context = 'stand-alone',
262 locale: Locale | str | None = None,
263) -> LocaleDataDict:
264 """Return the names for day periods (AM/PM) used by the locale.
266 >>> get_period_names(locale='en_US')['am']
267 'AM'
269 :param width: the width to use, one of "abbreviated", "narrow", or "wide"
270 :param context: the context, either "format" or "stand-alone"
271 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
272 """
273 return Locale.parse(locale or LC_TIME).day_periods[context][width]
276def get_day_names(
277 width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide',
278 context: _Context = 'format',
279 locale: Locale | str | None = None,
280) -> LocaleDataDict:
281 """Return the day names used by the locale for the specified format.
283 >>> get_day_names('wide', locale='en_US')[1]
284 'Tuesday'
285 >>> get_day_names('short', locale='en_US')[1]
286 'Tu'
287 >>> get_day_names('abbreviated', locale='es')[1]
288 'mar'
289 >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1]
290 'D'
292 :param width: the width to use, one of "wide", "abbreviated", "short" or "narrow"
293 :param context: the context, either "format" or "stand-alone"
294 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
295 """
296 return Locale.parse(locale or LC_TIME).days[context][width]
299def get_month_names(
300 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
301 context: _Context = 'format',
302 locale: Locale | str | None = None,
303) -> LocaleDataDict:
304 """Return the month names used by the locale for the specified format.
306 >>> get_month_names('wide', locale='en_US')[1]
307 'January'
308 >>> get_month_names('abbreviated', locale='es')[1]
309 'ene'
310 >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1]
311 'J'
313 :param width: the width to use, one of "wide", "abbreviated", or "narrow"
314 :param context: the context, either "format" or "stand-alone"
315 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
316 """
317 return Locale.parse(locale or LC_TIME).months[context][width]
320def get_quarter_names(
321 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
322 context: _Context = 'format',
323 locale: Locale | str | None = None,
324) -> LocaleDataDict:
325 """Return the quarter names used by the locale for the specified format.
327 >>> get_quarter_names('wide', locale='en_US')[1]
328 '1st quarter'
329 >>> get_quarter_names('abbreviated', locale='de_DE')[1]
330 'Q1'
331 >>> get_quarter_names('narrow', locale='de_DE')[1]
332 '1'
334 :param width: the width to use, one of "wide", "abbreviated", or "narrow"
335 :param context: the context, either "format" or "stand-alone"
336 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
337 """
338 return Locale.parse(locale or LC_TIME).quarters[context][width]
341def get_era_names(
342 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
343 locale: Locale | str | None = None,
344) -> LocaleDataDict:
345 """Return the era names used by the locale for the specified format.
347 >>> get_era_names('wide', locale='en_US')[1]
348 'Anno Domini'
349 >>> get_era_names('abbreviated', locale='de_DE')[1]
350 'n. Chr.'
352 :param width: the width to use, either "wide", "abbreviated", or "narrow"
353 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
354 """
355 return Locale.parse(locale or LC_TIME).eras[width]
358def get_relative_name(
359 field: str,
360 offset: int,
361 length: Literal['long', 'short', 'narrow'] = 'long',
362 locale: Locale | str | None = None,
363) -> str | None:
364 """Return the locale's literal name for a relative offset of a date field.
366 These are the CLDR ``<relative>`` strings such as "yesterday", "today",
367 "tomorrow" for ``field='day'``, or "last Monday"/"this Monday"/"next Monday"
368 for ``field='mon'``.
370 Returns ``None`` when the locale does not define a literal name for the
371 requested field/offset (in which case a phrase like "in 2 days" built
372 via :func:`format_timedelta` is typically the right fallback).
374 >>> get_relative_name('day', -1, locale='en')
375 'yesterday'
376 >>> get_relative_name('day', 1, locale='en')
377 'tomorrow'
378 >>> get_relative_name('day', -2, locale='de')
379 'vorgestern'
380 >>> get_relative_name('year', 1, locale='en')
381 'next year'
382 >>> get_relative_name('year', 1, length='short', locale='en')
383 'next yr.'
384 >>> get_relative_name('mon', 1, locale='en')
385 'next Monday'
387 Offsets without a literal name return ``None``:
389 >>> get_relative_name('day', 5, locale='en') is None
390 True
392 :param field: the date field, e.g. "day", "week", "month", "year",
393 "hour", "minute", "second", "quarter", or a weekday
394 abbreviation like "mon", "tue", etc.
395 :param offset: the offset from the current value of *field*.
396 :param length: "long", "short", or "narrow".
397 :param locale: a `Locale` object or a locale identifier. Defaults to the
398 system time locale.
400 .. versionadded:: 2.19
401 """
402 locale = Locale.parse(locale or LC_TIME)
403 date_fields = locale._data['date_fields']
404 key = field if length == 'long' else f'{field}-{length}'
405 data = date_fields.get(key) or date_fields.get(field) or {}
406 return data.get('relative', {}).get(offset)
409def get_date_format(
410 format: _PredefinedTimeFormat = 'medium',
411 locale: Locale | str | None = None,
412) -> DateTimePattern:
413 """Return the date formatting patterns used by the locale for the specified
414 format.
416 >>> get_date_format(locale='en_US')
417 <DateTimePattern 'MMM d, y'>
418 >>> get_date_format('full', locale='de_DE')
419 <DateTimePattern 'EEEE, d. MMMM y'>
421 :param format: the format to use, one of "full", "long", "medium", or
422 "short"
423 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
424 """
425 return Locale.parse(locale or LC_TIME).date_formats[format]
428def get_datetime_format(
429 format: _PredefinedTimeFormat = 'medium',
430 locale: Locale | str | None = None,
431) -> DateTimePattern:
432 """Return the datetime formatting patterns used by the locale for the
433 specified format.
435 >>> get_datetime_format(locale='en_US')
436 '{1}, {0}'
438 :param format: the format to use, one of "full", "long", "medium", or
439 "short"
440 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
441 """
442 patterns = Locale.parse(locale or LC_TIME).datetime_formats
443 if format not in patterns:
444 format = None
445 return patterns[format]
448def get_time_format(
449 format: _PredefinedTimeFormat = 'medium',
450 locale: Locale | str | None = None,
451) -> DateTimePattern:
452 """Return the time formatting patterns used by the locale for the specified
453 format.
455 >>> get_time_format(locale='en_US')
456 <DateTimePattern 'h:mm:ss\\u202fa'>
457 >>> get_time_format('full', locale='de_DE')
458 <DateTimePattern 'HH:mm:ss zzzz'>
460 :param format: the format to use, one of "full", "long", "medium", or
461 "short"
462 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
463 """
464 return Locale.parse(locale or LC_TIME).time_formats[format]
467def get_timezone_gmt(
468 datetime: _Instant = None,
469 width: Literal['long', 'short', 'iso8601', 'iso8601_short'] = 'long',
470 locale: Locale | str | None = None,
471 return_z: bool = False,
472 # TODO(3.0): Make return_z kw-only too
473 *,
474 utc: bool = False,
475) -> str:
476 """Return the timezone associated with the given `datetime` object formatted
477 as string indicating the offset from GMT.
479 >>> from datetime import datetime
480 >>> dt = datetime(2007, 4, 1, 15, 30)
481 >>> get_timezone_gmt(dt, locale='en')
482 'GMT+00:00'
483 >>> get_timezone_gmt(dt, locale='en', return_z=True)
484 'Z'
485 >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
486 '+00'
487 >>> tz = get_timezone('America/Los_Angeles')
488 >>> dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
489 >>> get_timezone_gmt(dt, locale='en')
490 'GMT-07:00'
491 >>> get_timezone_gmt(dt, 'short', locale='en')
492 '-0700'
493 >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
494 '-07'
496 The long format depends on the locale, for example in France the acronym
497 UTC string is used instead of GMT:
499 >>> get_timezone_gmt(dt, 'long', locale='fr_FR')
500 'UTC-07:00'
502 Passing ``utc=True`` requests the localized equivalent of "UTC" instead of
503 "GMT":
505 >>> get_timezone_gmt(dt, locale='en', utc=True)
506 'UTC-07:00'
507 >>> get_timezone_gmt(dt, locale='ar', utc=True)
508 'التوقيت العالمي -07:00'
510 .. versionadded:: 0.9
512 :param datetime: the ``datetime`` object; if `None`, the current date and
513 time in UTC is used
514 :param width: either "long" or "short" or "iso8601" or "iso8601_short"
515 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
516 :param return_z: True or False; Function returns indicator "Z"
517 when local time offset is 0
518 :param utc: If ``True``, use the localized equivalent of "UTC" instead of
519 "GMT" in the long format. Only affects ``width='long'`` as
520 other widths have no GMT/UTC prefix.
522 .. versionadded:: 2.19
523 """
524 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime))
525 locale = Locale.parse(locale or LC_TIME)
527 offset = datetime.tzinfo.utcoffset(datetime)
528 seconds = offset.days * 24 * 60 * 60 + offset.seconds
529 if return_z and seconds == 0:
530 return 'Z'
531 sign = '-' if seconds < 0 else '+'
532 hours, seconds = divmod(abs(seconds), 3600)
533 if seconds == 0 and width == 'iso8601_short':
534 return '%s%02d' % (sign, hours)
535 elif width == 'short' or width == 'iso8601_short':
536 pattern = '%s%02d%02d'
537 elif width == 'iso8601':
538 pattern = '%s%02d:%02d'
539 else:
540 pattern = locale.zone_formats['gmt_utc' if utc else 'gmt'] % '%s%02d:%02d'
541 return pattern % (sign, hours, seconds // 60)
544def get_timezone_location(
545 dt_or_tzinfo: _DtOrTzinfo = None,
546 locale: Locale | str | None = None,
547 return_city: bool = False,
548) -> str:
549 """Return a representation of the given timezone using "location format".
551 The result depends on both the local display name of the country and the
552 city associated with the time zone:
554 >>> tz = get_timezone('America/St_Johns')
555 >>> print(get_timezone_location(tz, locale='de_DE'))
556 Kanada (St. John’s) (Ortszeit)
557 >>> print(get_timezone_location(tz, locale='en'))
558 Canada (St. John’s) Time
559 >>> print(get_timezone_location(tz, locale='en', return_city=True))
560 St. John’s
561 >>> tz = get_timezone('America/Mexico_City')
562 >>> get_timezone_location(tz, locale='de_DE')
563 'Mexiko (Mexiko-Stadt) (Ortszeit)'
565 If the timezone is associated with a country that uses only a single
566 timezone, just the localized country name is returned:
568 >>> tz = get_timezone('Europe/Berlin')
569 >>> get_timezone_name(tz, locale='de_DE')
570 'Mitteleuropäische Zeit'
572 .. versionadded:: 0.9
574 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines
575 the timezone; if `None`, the current date and time in
576 UTC is assumed
577 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
578 :param return_city: True or False, if True then return exemplar city (location)
579 for the time zone
580 :return: the localized timezone name using location format
582 """
583 locale = Locale.parse(locale or LC_TIME)
585 zone = _get_tz_name(dt_or_tzinfo)
587 # Get the canonical time-zone code
588 zone = get_global('zone_aliases').get(zone, zone)
590 info = locale.time_zones.get(zone, {})
592 # Otherwise, if there is only one timezone for the country, return the
593 # localized country name
594 region_format = locale.zone_formats['region']
595 territory = get_global('zone_territories').get(zone)
596 if territory not in locale.territories:
597 territory = 'ZZ' # invalid/unknown
598 territory_name = locale.territories.get(territory)
599 if (
600 not return_city
601 and territory_name
602 and len(get_global('territory_zones').get(territory, [])) == 1
603 ):
604 return region_format % territory_name
606 # Otherwise, include the city in the output
607 fallback_format = locale.zone_formats['fallback']
608 if 'city' in info:
609 city_name = info['city']
610 else:
611 metazone = get_global('meta_zones').get(zone)
612 metazone_info = locale.meta_zones.get(metazone, {})
613 if 'city' in metazone_info:
614 city_name = metazone_info['city']
615 else:
616 city_name = zone.rpartition('/')[2].replace('_', ' ')
618 if return_city:
619 return city_name
620 if territory_name is None:
621 # The locale has no name for the zone's region, not even for the
622 # "unknown region" fallback, so the region is left out.
623 return region_format % city_name
624 return region_format % (
625 fallback_format
626 % {
627 '0': city_name,
628 '1': territory_name,
629 }
630 )
633def get_timezone_name(
634 dt_or_tzinfo: _DtOrTzinfo = None,
635 width: Literal['long', 'short'] = 'long',
636 uncommon: bool = False,
637 locale: Locale | str | None = None,
638 zone_variant: Literal['generic', 'daylight', 'standard'] | None = None,
639 return_zone: bool = False,
640) -> str:
641 r"""Return the localized display name for the given timezone. The timezone
642 may be specified using a ``datetime`` or `tzinfo` object.
644 >>> from datetime import time
645 >>> dt = time(15, 30, tzinfo=get_timezone('America/Los_Angeles'))
646 >>> get_timezone_name(dt, locale='en_US') # doctest: +SKIP
647 'Pacific Standard Time'
648 >>> get_timezone_name(dt, locale='en_US', return_zone=True)
649 'America/Los_Angeles'
650 >>> get_timezone_name(dt, width='short', locale='en_US') # doctest: +SKIP
651 'PST'
653 If this function gets passed only a `tzinfo` object and no concrete
654 `datetime`, the returned display name is independent of daylight savings
655 time. This can be used for example for selecting timezones, or to set the
656 time of events that recur across DST changes:
658 >>> tz = get_timezone('America/Los_Angeles')
659 >>> get_timezone_name(tz, locale='en_US')
660 'Pacific Time'
661 >>> get_timezone_name(tz, 'short', locale='en_US')
662 'PT'
664 If no localized display name for the timezone is available, and the timezone
665 is associated with a country that uses only a single timezone, the name of
666 that country is returned, formatted according to the locale:
668 >>> tz = get_timezone('Europe/Berlin')
669 >>> get_timezone_name(tz, locale='de_DE')
670 'Mitteleuropäische Zeit'
671 >>> get_timezone_name(tz, locale='pt_BR')
672 'Horário da Europa Central'
674 On the other hand, if the country uses multiple timezones, the city is also
675 included in the representation:
677 >>> tz = get_timezone('America/St_Johns')
678 >>> get_timezone_name(tz, locale='de_DE')
679 'Neufundland-Zeit'
681 Note that short format is currently not supported for all timezones and
682 all locales. This is partially because not every timezone has a short
683 code in every locale. In that case it currently falls back to the long
684 format.
686 For more information see `LDML Appendix J: Time Zone Display Names
687 <https://www.unicode.org/reports/tr35/#Time_Zone_Fallback>`_
689 .. versionadded:: 0.9
691 .. versionchanged:: 1.0
692 Added `zone_variant` support.
694 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines
695 the timezone; if a ``tzinfo`` object is used, the
696 resulting display name will be generic, i.e.
697 independent of daylight savings time; if `None`, the
698 current date in UTC is assumed
699 :param width: either "long" or "short"
700 :param uncommon: deprecated and ignored
701 :param zone_variant: defines the zone variation to return. By default the
702 variation is defined from the datetime object
703 passed in. If no datetime object is passed in, the
704 ``'generic'`` variation is assumed. The following
705 values are valid: ``'generic'``, ``'daylight'`` and
706 ``'standard'``.
707 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
708 :param return_zone: True or False. If true then function
709 returns long time zone ID
710 """
711 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
712 locale = Locale.parse(locale or LC_TIME)
714 zone = _get_tz_name(dt_or_tzinfo)
716 if zone_variant is None:
717 if dt is None:
718 zone_variant = 'generic'
719 else:
720 dst = tzinfo.dst(dt)
721 zone_variant = "daylight" if dst else "standard"
722 else:
723 if zone_variant not in ('generic', 'standard', 'daylight'):
724 raise ValueError('Invalid zone variation')
726 # Get the canonical time-zone code
727 zone = get_global('zone_aliases').get(zone, zone)
728 if return_zone:
729 return zone
730 info = locale.time_zones.get(zone, {})
731 # Try explicitly translated zone names first
732 if width in info and zone_variant in info[width]:
733 value = info[width][zone_variant]
734 if value != NO_INHERITANCE_MARKER:
735 return value
737 metazone = get_global('meta_zones').get(zone)
738 if metazone:
739 metazone_info = locale.meta_zones.get(metazone, {})
740 if width in metazone_info:
741 name = metazone_info[width].get(zone_variant)
742 if width == 'short' and name == NO_INHERITANCE_MARKER:
743 # If the short form is marked no-inheritance,
744 # try to fall back to the long name instead.
745 name = metazone_info.get('long', {}).get(zone_variant)
746 if name and name != NO_INHERITANCE_MARKER:
747 return name
749 # If we have a concrete datetime, we assume that the result can't be
750 # independent of daylight savings time, so we return the GMT offset
751 if dt is not None:
752 return get_timezone_gmt(dt, width=width, locale=locale)
754 return get_timezone_location(dt_or_tzinfo, locale=locale)
757def format_date(
758 date: datetime.date | None = None,
759 format: _PredefinedTimeFormat | str = 'medium',
760 locale: Locale | str | None = None,
761) -> str:
762 """Return a date formatted according to the given pattern.
764 >>> from datetime import date
765 >>> d = date(2007, 4, 1)
766 >>> format_date(d, locale='en_US')
767 'Apr 1, 2007'
768 >>> format_date(d, format='full', locale='de_DE')
769 'Sonntag, 1. April 2007'
771 If you don't want to use the locale default formats, you can specify a
772 custom date pattern:
774 >>> format_date(d, "EEE, MMM d, ''yy", locale='en')
775 "Sun, Apr 1, '07"
777 :param date: the ``date`` or ``datetime`` object; if `None`, the current
778 date is used
779 :param format: one of "full", "long", "medium", or "short", or a custom
780 date/time pattern
781 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
782 """
783 if date is None:
784 date = datetime.date.today()
785 elif isinstance(date, datetime.datetime):
786 date = date.date()
788 locale = Locale.parse(locale or LC_TIME)
789 if format in ('full', 'long', 'medium', 'short'):
790 format = get_date_format(format, locale=locale)
791 pattern = parse_pattern(format)
792 return pattern.apply(date, locale)
795def format_datetime(
796 datetime: _Instant = None,
797 format: _PredefinedTimeFormat | str = 'medium',
798 tzinfo: datetime.tzinfo | None = None,
799 locale: Locale | str | None = None,
800) -> str:
801 r"""Return a date formatted according to the given pattern.
803 >>> from datetime import datetime
804 >>> dt = datetime(2007, 4, 1, 15, 30)
805 >>> format_datetime(dt, locale='en_US')
806 'Apr 1, 2007, 3:30:00\u202fPM'
808 For any pattern requiring the display of the timezone:
810 >>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'),
811 ... locale='fr_FR')
812 'dimanche 1 avril 2007, 17:30:00 heure d’été d’Europe centrale'
813 >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
814 ... tzinfo=get_timezone('US/Eastern'), locale='en')
815 '2007.04.01 AD at 11:30:00 EDT'
817 :param datetime: the `datetime` object; if `None`, the current date and
818 time is used
819 :param format: one of "full", "long", "medium", or "short", or a custom
820 date/time pattern
821 :param tzinfo: the timezone to apply to the time for display
822 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
823 """
824 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime), tzinfo)
826 locale = Locale.parse(locale or LC_TIME)
827 if format in ('full', 'long', 'medium', 'short'):
828 return (
829 get_datetime_format(format, locale=locale)
830 .replace("'", "")
831 .replace('{0}', format_time(datetime, format, tzinfo=None, locale=locale))
832 .replace('{1}', format_date(datetime, format, locale=locale))
833 )
834 else:
835 return parse_pattern(format).apply(datetime, locale)
838def format_relative_datetime(
839 relative_date: str,
840 time: datetime.time | datetime.datetime | float | None = None,
841 format: _PredefinedTimeFormat = 'medium',
842 tzinfo: datetime.tzinfo | None = None,
843 locale: Locale | str | None = None,
844) -> str:
845 """Combine a pre-formatted relative date with a time using the locale's
846 relative datetime pattern.
848 For instance, in English you might say "tomorrow at 3:30 PM" instead of
849 "tomorrow, 3:30 PM".
851 The caller supplies the relative date portion as a string
852 (such as "tomorrow" or "in 3 days"); this function formats the time and joins the two.
854 >>> from datetime import time
855 >>> format_relative_datetime('tomorrow', time(15, 30), locale='en', format='long')
856 'tomorrow at 3:30:00\u202fPM UTC'
857 >>> format_relative_datetime('morgen', time(15, 30), locale='de', format='long')
858 'morgen um 15:30:00 UTC'
860 When the locale does not define a relative pattern for the requested
861 width, its standard datetime pattern is used, as CLDR's root locale
862 aliases ``relative`` patterns to ``standard`` ones:
864 >>> format_relative_datetime('zítra', time(15, 30), locale='cs', format='short')
865 'zítra 15:30'
867 :param relative_date: the already-localized relative date string.
868 :param time: the time to combine with the relative date.
869 If ``None`` the current time is used.
870 :param format: one of "full", "long", "medium", or "short".
871 :param tzinfo: the timezone to apply to the time for display.
872 :param locale: a `Locale` object or a locale identifier.
873 Defaults to the system time locale.
875 .. versionadded:: 2.19
876 """
877 locale = Locale.parse(locale or LC_TIME)
878 pattern = (
879 locale.datetime_formats_relative.get(format)
880 or get_datetime_format(format, locale=locale)
881 )
882 return (
883 pattern
884 .replace("'", "")
885 .replace('{0}', format_time(time, format, tzinfo=tzinfo, locale=locale))
886 .replace('{1}', relative_date)
887 )
890def format_time(
891 time: datetime.time | datetime.datetime | float | None = None,
892 format: _PredefinedTimeFormat | str = 'medium',
893 tzinfo: datetime.tzinfo | None = None,
894 locale: Locale | str | None = None,
895) -> str:
896 r"""Return a time formatted according to the given pattern.
898 >>> from datetime import datetime, time
899 >>> t = time(15, 30)
900 >>> format_time(t, locale='en_US')
901 '3:30:00\u202fPM'
902 >>> format_time(t, format='short', locale='de_DE')
903 '15:30'
905 If you don't want to use the locale default formats, you can specify a
906 custom time pattern:
908 >>> format_time(t, "hh 'o''clock' a", locale='en')
909 "03 o'clock PM"
911 For any pattern requiring the display of the time-zone a
912 timezone has to be specified explicitly:
914 >>> t = datetime(2007, 4, 1, 15, 30)
915 >>> tzinfo = get_timezone('Europe/Paris')
916 >>> t = _localize(tzinfo, t)
917 >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR')
918 '15:30:00 heure d’été d’Europe centrale'
919 >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'),
920 ... locale='en')
921 "09 o'clock AM, Eastern Daylight Time"
923 As that example shows, when this function gets passed a
924 ``datetime.datetime`` value, the actual time in the formatted string is
925 adjusted to the timezone specified by the `tzinfo` parameter. If the
926 ``datetime`` is "naive" (i.e. it has no associated timezone information),
927 it is assumed to be in UTC.
929 These timezone calculations are **not** performed if the value is of type
930 ``datetime.time``, as without date information there's no way to determine
931 what a given time would translate to in a different timezone without
932 information about whether daylight savings time is in effect or not. This
933 means that time values are left as-is, and the value of the `tzinfo`
934 parameter is only used to display the timezone name if needed:
936 >>> t = time(15, 30)
937 >>> format_time(t, format='full', tzinfo=get_timezone('Europe/Paris'),
938 ... locale='fr_FR') # doctest: +SKIP
939 '15:30:00 heure normale d\u2019Europe centrale'
940 >>> format_time(t, format='full', tzinfo=get_timezone('US/Eastern'),
941 ... locale='en_US') # doctest: +SKIP
942 '3:30:00\u202fPM Eastern Standard Time'
944 :param time: the ``time`` or ``datetime`` object; if `None`, the current
945 time in UTC is used
946 :param format: one of "full", "long", "medium", or "short", or a custom
947 date/time pattern
948 :param tzinfo: the time-zone to apply to the time for display
949 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
950 """
952 # get reference date for if we need to find the right timezone variant
953 # in the pattern
954 ref_date = time.date() if isinstance(time, datetime.datetime) else None
956 time = _get_time(time, tzinfo)
958 locale = Locale.parse(locale or LC_TIME)
959 if format in ('full', 'long', 'medium', 'short'):
960 format = get_time_format(format, locale=locale)
961 return parse_pattern(format).apply(time, locale, reference_date=ref_date)
964def format_skeleton(
965 skeleton: str,
966 datetime: _Instant = None,
967 tzinfo: datetime.tzinfo | None = None,
968 fuzzy: bool = True,
969 locale: Locale | str | None = None,
970) -> str:
971 r"""Return a time and/or date formatted according to the given pattern.
973 The skeletons are defined in the CLDR data and provide more flexibility
974 than the simple short/long/medium formats, but are a bit harder to use.
975 The are defined using the date/time symbols without order or punctuation
976 and map to a suitable format for the given locale.
978 >>> from datetime import datetime
979 >>> t = datetime(2007, 4, 1, 15, 30)
980 >>> format_skeleton('MMMEd', t, locale='fr')
981 'dim. 1 avr.'
982 >>> format_skeleton('MMMEd', t, locale='en')
983 'Sun, Apr 1'
984 >>> format_skeleton('yMMd', t, locale='fi') # yMMd is not in the Finnish locale; yMd gets used
985 '1.4.2007'
986 >>> format_skeleton('yMMd', t, fuzzy=False, locale='fi') # yMMd is not in the Finnish locale, an error is thrown
987 Traceback (most recent call last):
988 ...
989 KeyError: yMMd
990 >>> format_skeleton('GH', t, fuzzy=True, locale='fi_FI') # GH is not in the Finnish locale and there is no close match, an error is thrown
991 Traceback (most recent call last):
992 ...
993 KeyError: 'GH'
995 After the skeleton is resolved to a pattern `format_datetime` is called so
996 all timezone processing etc is the same as for that.
998 :param skeleton: A date time skeleton as defined in the cldr data.
999 :param datetime: the ``time`` or ``datetime`` object; if `None`, the current
1000 time in UTC is used
1001 :param tzinfo: the time-zone to apply to the time for display
1002 :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's
1003 close enough to it. If there is no close match, a `KeyError`
1004 with the requested skeleton is thrown.
1005 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
1006 """
1007 locale = Locale.parse(locale or LC_TIME)
1008 if fuzzy and skeleton not in locale.datetime_skeletons:
1009 skeleton = match_skeleton(skeleton, locale.datetime_skeletons) or skeleton
1010 format = locale.datetime_skeletons[skeleton]
1011 return format_datetime(datetime, format, tzinfo, locale)
1014TIMEDELTA_UNITS: tuple[tuple[str, int], ...] = (
1015 ('year', 3600 * 24 * 365),
1016 ('month', 3600 * 24 * 30),
1017 ('week', 3600 * 24 * 7),
1018 ('day', 3600 * 24),
1019 ('hour', 3600),
1020 ('minute', 60),
1021 ('second', 1),
1022)
1025def format_timedelta(
1026 delta: datetime.timedelta | int,
1027 granularity: Literal[
1028 'year',
1029 'month',
1030 'week',
1031 'day',
1032 'hour',
1033 'minute',
1034 'second',
1035 ] = 'second',
1036 threshold: float = 0.85,
1037 add_direction: bool = False,
1038 format: Literal['narrow', 'short', 'medium', 'long'] = 'long',
1039 locale: Locale | str | None = None,
1040) -> str:
1041 """Return a time delta according to the rules of the given locale.
1043 >>> from datetime import timedelta
1044 >>> format_timedelta(timedelta(weeks=12), locale='en_US')
1045 '3 months'
1046 >>> format_timedelta(timedelta(seconds=1), locale='es')
1047 '1 segundo'
1049 The granularity parameter can be provided to alter the lowest unit
1050 presented, which defaults to a second.
1052 >>> format_timedelta(timedelta(hours=3), granularity='day', locale='en_US')
1053 '1 day'
1055 The threshold parameter can be used to determine at which value the
1056 presentation switches to the next higher unit. A higher threshold factor
1057 means the presentation will switch later. For example:
1059 >>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US')
1060 '1 day'
1061 >>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US')
1062 '23 hours'
1064 In addition directional information can be provided that informs
1065 the user if the date is in the past or in the future:
1067 >>> format_timedelta(timedelta(hours=1), add_direction=True, locale='en')
1068 'in 1 hour'
1069 >>> format_timedelta(timedelta(hours=-1), add_direction=True, locale='en')
1070 '1 hour ago'
1072 The format parameter controls how compact or wide the presentation is:
1074 >>> format_timedelta(timedelta(hours=3), format='short', locale='en')
1075 '3 hr'
1076 >>> format_timedelta(timedelta(hours=3), format='narrow', locale='en')
1077 '3h'
1079 :param delta: a ``timedelta`` object representing the time difference to
1080 format, or the delta in seconds as an `int` value
1081 :param granularity: determines the smallest unit that should be displayed,
1082 the value can be one of "year", "month", "week", "day",
1083 "hour", "minute" or "second"
1084 :param threshold: factor that determines at which point the presentation
1085 switches to the next higher unit
1086 :param add_direction: if this flag is set to `True` the return value will
1087 include directional information. For instance a
1088 positive timedelta will include the information about
1089 it being in the future, a negative will be information
1090 about the value being in the past.
1091 :param format: the format, can be "narrow", "short" or "long". (
1092 "medium" is deprecated, currently converted to "long" to
1093 maintain compatibility)
1094 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
1095 """
1096 if format not in ('narrow', 'short', 'medium', 'long'):
1097 raise TypeError('Format must be one of "narrow", "short" or "long"')
1098 if format == 'medium':
1099 warnings.warn(
1100 '"medium" value for format param of format_timedelta is deprecated. Use "long" instead',
1101 category=DeprecationWarning,
1102 stacklevel=2,
1103 )
1104 format = 'long'
1105 if isinstance(delta, datetime.timedelta):
1106 seconds = delta.total_seconds()
1107 else:
1108 seconds = delta
1109 locale = Locale.parse(locale or LC_TIME)
1110 date_fields = locale._data["date_fields"]
1111 unit_patterns = locale._data["unit_patterns"]
1113 def _iter_patterns(a_unit):
1114 if add_direction:
1115 # Try to find the length variant version first ("year-narrow")
1116 # before falling back to the default.
1117 unit_rel_patterns = date_fields.get(f"{a_unit}-{format}") or date_fields.get(a_unit) or {}
1118 if seconds >= 0:
1119 yield unit_rel_patterns['future']
1120 else:
1121 yield unit_rel_patterns['past']
1122 a_unit = f"duration-{a_unit}"
1123 yield unit_patterns.get(a_unit, {}).get(format) # resolves aliases
1125 for unit, secs_per_unit in TIMEDELTA_UNITS:
1126 value = abs(seconds) / secs_per_unit
1127 if value >= threshold or unit == granularity:
1128 if unit == granularity and value > 0:
1129 value = max(1, value)
1130 value = int(round(value))
1131 plural_form = locale.plural_form(value)
1132 pattern = None
1133 for patterns in _iter_patterns(unit):
1134 if patterns is not None:
1135 pattern = patterns.get(plural_form) or patterns.get('other')
1136 if pattern:
1137 break
1138 # This really should not happen
1139 if pattern is None:
1140 return ''
1141 return pattern.replace('{0}', str(value))
1143 return ''
1146def _format_fallback_interval(
1147 start: _Instant,
1148 end: _Instant,
1149 skeleton: str | None,
1150 tzinfo: datetime.tzinfo | None,
1151 locale: Locale,
1152) -> str:
1153 if skeleton in locale.datetime_skeletons: # Use the given skeleton
1154 format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale)
1155 elif all(
1156 # Both are just dates
1157 (isinstance(d, datetime.date) and not isinstance(d, datetime.datetime))
1158 for d in (start, end)
1159 ):
1160 format = lambda dt: format_date(dt, locale=locale)
1161 elif all(
1162 # Both are times
1163 (isinstance(d, datetime.time) and not isinstance(d, datetime.date))
1164 for d in (start, end)
1165 ):
1166 format = lambda dt: format_time(dt, tzinfo=tzinfo, locale=locale)
1167 else:
1168 format = lambda dt: format_datetime(dt, tzinfo=tzinfo, locale=locale)
1170 formatted_start = format(start)
1171 formatted_end = format(end)
1173 if formatted_start == formatted_end:
1174 return format(start)
1176 return (
1177 locale.interval_formats.get(None, "{0}-{1}")
1178 .replace("{0}", formatted_start)
1179 .replace("{1}", formatted_end)
1180 )
1183def format_interval(
1184 start: _Instant,
1185 end: _Instant,
1186 skeleton: str | None = None,
1187 tzinfo: datetime.tzinfo | None = None,
1188 fuzzy: bool = True,
1189 locale: Locale | str | None = None,
1190) -> str:
1191 """
1192 Format an interval between two instants according to the locale's rules.
1194 >>> from datetime import date, time
1195 >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "yMd", locale="fi")
1196 '15.–17.1.2016'
1198 >>> format_interval(time(12, 12), time(16, 16), "Hm", locale="en_GB")
1199 '12:12–16:16'
1201 >>> format_interval(time(5, 12), time(16, 16), "hm", locale="en_US")
1202 '5:12\\u202fAM\\u2009–\\u20094:16\\u202fPM'
1204 >>> format_interval(time(16, 18), time(16, 24), "Hm", locale="it")
1205 '16:18–16:24'
1207 If the start instant equals the end instant, the interval is formatted like the instant.
1209 >>> format_interval(time(16, 18), time(16, 18), "Hm", locale="it")
1210 '16:18'
1212 Unknown skeletons fall back to "default" formatting.
1214 >>> format_interval(date(2015, 1, 1), date(2017, 1, 1), "wzq", locale="ja")
1215 '2015/01/01~2017/01/01'
1217 >>> format_interval(time(16, 18), time(16, 24), "xxx", locale="ja")
1218 '16:18:00~16:24:00'
1220 >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "xxx", locale="de")
1221 '15.01.2016\\u2009–\\u200917.01.2016'
1223 :param start: First instant (datetime/date/time)
1224 :param end: Second instant (datetime/date/time)
1225 :param skeleton: The "skeleton format" to use for formatting.
1226 :param tzinfo: tzinfo to use (if none is already attached)
1227 :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's
1228 close enough to it.
1229 :param locale: A locale object or identifier. Defaults to the system time locale.
1230 :return: Formatted interval
1231 """
1232 locale = Locale.parse(locale or LC_TIME)
1234 # NB: The quote comments below are from the algorithm description in
1235 # https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats
1237 # > Look for the intervalFormatItem element that matches the "skeleton",
1238 # > starting in the current locale and then following the locale fallback
1239 # > chain up to, but not including root.
1241 interval_formats = locale.interval_formats
1243 if skeleton not in interval_formats or not skeleton:
1244 # > If no match was found from the previous step, check what the closest
1245 # > match is in the fallback locale chain, as in availableFormats. That
1246 # > is, this allows for adjusting the string value field's width,
1247 # > including adjusting between "MMM" and "MMMM", and using different
1248 # > variants of the same field, such as 'v' and 'z'.
1249 if skeleton and fuzzy:
1250 skeleton = match_skeleton(skeleton, interval_formats)
1251 else:
1252 skeleton = None
1253 if not skeleton: # Still no match whatsoever?
1254 # > Otherwise, format the start and end datetime using the fallback pattern.
1255 return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
1257 skel_formats = interval_formats[skeleton]
1259 if start == end:
1260 return format_skeleton(skeleton, start, tzinfo, fuzzy=fuzzy, locale=locale)
1262 start = _ensure_datetime_tzinfo(_get_datetime(start), tzinfo=tzinfo)
1263 end = _ensure_datetime_tzinfo(_get_datetime(end), tzinfo=tzinfo)
1265 start_fmt = DateTimeFormat(start, locale=locale)
1266 end_fmt = DateTimeFormat(end, locale=locale)
1268 # > If a match is found from previous steps, compute the calendar field
1269 # > with the greatest difference between start and end datetime. If there
1270 # > is no difference among any of the fields in the pattern, format as a
1271 # > single date using availableFormats, and return.
1273 for field in PATTERN_CHAR_ORDER: # These are in largest-to-smallest order
1274 if field in skel_formats and start_fmt.extract(field) != end_fmt.extract(field):
1275 # > If there is a match, use the pieces of the corresponding pattern to
1276 # > format the start and end datetime, as above.
1277 return "".join(
1278 parse_pattern(pattern).apply(instant, locale)
1279 for pattern, instant in zip(skel_formats[field], (start, end))
1280 )
1282 # > Otherwise, format the start and end datetime using the fallback pattern.
1284 return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
1287def get_period_id(
1288 time: _Instant,
1289 tzinfo: datetime.tzinfo | None = None,
1290 type: Literal['selection'] | None = None,
1291 locale: Locale | str | None = None,
1292) -> str:
1293 """
1294 Get the day period ID for a given time.
1296 This ID can be used as a key for the period name dictionary.
1298 >>> from datetime import time
1299 >>> get_period_names(locale="de")[get_period_id(time(7, 42), locale="de")]
1300 'Morgen'
1302 >>> get_period_id(time(0), locale="en_US")
1303 'midnight'
1305 >>> get_period_id(time(0), type="selection", locale="en_US")
1306 'morning1'
1308 :param time: The time to inspect.
1309 :param tzinfo: The timezone for the time. See ``format_time``.
1310 :param type: The period type to use. Either "selection" or None.
1311 The selection type is used for selecting among phrases such as
1312 “Your email arrived yesterday evening” or “Your email arrived last night”.
1313 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
1314 :return: period ID. Something is always returned -- even if it's just "am" or "pm".
1315 """
1316 time = _get_time(time, tzinfo)
1317 seconds_past_midnight = int(time.hour * 60 * 60 + time.minute * 60 + time.second)
1318 locale = Locale.parse(locale or LC_TIME)
1320 # The LDML rules state that the rules may not overlap, so iterating in arbitrary
1321 # order should be alright, though `at` periods should be preferred.
1322 rulesets = locale.day_period_rules.get(type, {}).items()
1324 for rule_id, rules in rulesets:
1325 for rule in rules:
1326 if "at" in rule and rule["at"] == seconds_past_midnight:
1327 return rule_id
1329 for rule_id, rules in rulesets:
1330 for rule in rules:
1331 if "from" in rule and "before" in rule:
1332 if rule["from"] < rule["before"]:
1333 if rule["from"] <= seconds_past_midnight < rule["before"]:
1334 return rule_id
1335 else:
1336 # e.g. from="21:00" before="06:00"
1337 if (
1338 rule["from"] <= seconds_past_midnight < 86400
1339 or 0 <= seconds_past_midnight < rule["before"]
1340 ):
1341 return rule_id
1343 start_ok = end_ok = False
1345 if "from" in rule and seconds_past_midnight >= rule["from"]:
1346 start_ok = True
1347 if "to" in rule and seconds_past_midnight <= rule["to"]:
1348 # This rule type does not exist in the present CLDR data;
1349 # excuse the lack of test coverage.
1350 end_ok = True
1351 if "before" in rule and seconds_past_midnight < rule["before"]:
1352 end_ok = True
1353 if "after" in rule:
1354 raise NotImplementedError("'after' is deprecated as of CLDR 29.")
1356 if start_ok and end_ok:
1357 return rule_id
1359 if seconds_past_midnight < 43200:
1360 return "am"
1361 else:
1362 return "pm"
1365class ParseError(ValueError):
1366 pass
1369def parse_date(
1370 string: str,
1371 locale: Locale | str | None = None,
1372 format: _PredefinedTimeFormat | str = 'medium',
1373) -> datetime.date:
1374 """Parse a date from a string.
1376 If an explicit format is provided, it is used to parse the date.
1378 >>> parse_date('01.04.2004', format='dd.MM.yyyy')
1379 datetime.date(2004, 4, 1)
1381 If no format is given, or if it is one of "full", "long", "medium",
1382 or "short", the function first tries to interpret the string as
1383 ISO-8601 date format and then uses the date format for the locale
1384 as a hint to determine the order in which the date fields appear in
1385 the string.
1387 >>> parse_date('4/1/04', locale='en_US')
1388 datetime.date(2004, 4, 1)
1389 >>> parse_date('01.04.2004', locale='de_DE')
1390 datetime.date(2004, 4, 1)
1391 >>> parse_date('2004-04-01', locale='en_US')
1392 datetime.date(2004, 4, 1)
1393 >>> parse_date('2004-04-01', locale='de_DE')
1394 datetime.date(2004, 4, 1)
1395 >>> parse_date('01.04.04', locale='de_DE', format='short')
1396 datetime.date(2004, 4, 1)
1398 :param string: the string containing the date
1399 :param locale: a `Locale` object or a locale identifier
1400 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
1401 :param format: the format to use, either an explicit date format,
1402 or one of "full", "long", "medium", or "short"
1403 (see ``get_time_format``)
1404 """
1405 numbers = re.findall(r'(\d+)', string)
1406 if not numbers:
1407 raise ParseError("No numbers were found in input")
1409 use_predefined_format = format in ('full', 'long', 'medium', 'short')
1410 # we try ISO-8601 format first, meaning similar to formats
1411 # extended YYYY-MM-DD or basic YYYYMMDD
1412 iso_alike = re.match(
1413 r'^(\d{4})-?([01]\d)-?([0-3]\d)$',
1414 string,
1415 flags=re.ASCII, # allow only ASCII digits
1416 )
1417 if iso_alike and use_predefined_format:
1418 try:
1419 return datetime.date(*map(int, iso_alike.groups()))
1420 except ValueError:
1421 pass # a locale format might fit better, so let's continue
1423 if use_predefined_format:
1424 fmt = get_date_format(format=format, locale=locale)
1425 else:
1426 fmt = parse_pattern(format)
1427 format_str = fmt.pattern.lower()
1428 year_idx = format_str.index('y')
1429 month_idx = format_str.find('m')
1430 if month_idx < 0:
1431 month_idx = format_str.index('l')
1432 day_idx = format_str.index('d')
1434 indexes = sorted([(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')])
1435 indexes = {item[1]: idx for idx, item in enumerate(indexes)}
1437 # FIXME: this currently only supports numbers, but should also support month
1438 # names, both in the requested locale, and english
1440 year = numbers[indexes['Y']]
1441 year = 2000 + int(year) if len(year) == 2 else int(year)
1442 month = int(numbers[indexes['M']])
1443 day = int(numbers[indexes['D']])
1444 if month > 12:
1445 month, day = day, month
1446 try:
1447 return datetime.date(year, month, day)
1448 except ValueError as e:
1449 raise ParseError(
1450 f"'{string}' does not represent a valid date for format '{fmt.pattern}'",
1451 ) from e
1454def parse_time(
1455 string: str,
1456 locale: Locale | str | None = None,
1457 format: _PredefinedTimeFormat | str = 'medium',
1458) -> datetime.time:
1459 """Parse a time from a string.
1461 This function uses the time format for the locale as a hint to determine
1462 the order in which the time fields appear in the string.
1464 If an explicit format is provided, the function will use it to parse
1465 the time instead.
1467 >>> parse_time('15:30:00', locale='en_US')
1468 datetime.time(15, 30)
1469 >>> parse_time('15:30:00', format='H:mm:ss')
1470 datetime.time(15, 30)
1472 :param string: the string containing the time
1473 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
1474 :param format: the format to use, either an explicit time format,
1475 or one of "full", "long", "medium", or "short"
1476 (see ``get_time_format``)
1477 :return: the parsed time
1478 :rtype: `time`
1479 """
1480 numbers = re.findall(r'(\d+)', string)
1481 if not numbers:
1482 raise ParseError("No numbers were found in input")
1484 # TODO: try ISO format first?
1485 if format in ('full', 'long', 'medium', 'short'):
1486 fmt = get_time_format(format=format, locale=locale)
1487 else:
1488 fmt = parse_pattern(format)
1489 format_str = fmt.pattern.lower()
1490 hour_idx = format_str.find('h')
1491 if hour_idx < 0:
1492 hour_idx = format_str.index('k')
1493 min_idx = format_str.index('m')
1494 # format might not contain seconds
1495 if (sec_idx := format_str.find('s')) < 0:
1496 sec_idx = math.inf
1498 indexes = sorted([(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')])
1499 indexes = {item[1]: idx for idx, item in enumerate(indexes)}
1501 # TODO: support time zones
1503 # Check if the format specifies a period to be used;
1504 # if it does, convert the hour from the 12-hour clock, on which
1505 # 12 AM and 12 PM are 00:00 and 12:00 respectively.
1506 hour = int(numbers[indexes['H']])
1507 if 'a' in format_str:
1508 if 'pm' in string.lower():
1509 if hour != 12:
1510 hour += 12
1511 elif 'am' in string.lower() and hour == 12:
1512 hour = 0
1514 # Parse up to three numbers from the string.
1515 minute = second = 0
1516 if len(numbers) > 1:
1517 minute = int(numbers[indexes['M']])
1518 if len(numbers) > 2:
1519 second = int(numbers[indexes['S']])
1520 try:
1521 return datetime.time(hour, minute, second)
1522 except ValueError as e:
1523 raise ParseError(
1524 f"'{string}' does not represent a valid time for format '{fmt.pattern}'",
1525 ) from e
1528class DateTimePattern:
1529 def __init__(self, pattern: str, format: DateTimeFormat):
1530 self.pattern = pattern
1531 self.format = format
1533 def __repr__(self) -> str:
1534 return f"<{type(self).__name__} {self.pattern!r}>"
1536 def __str__(self) -> str:
1537 pat = self.pattern
1538 return pat
1540 def __mod__(self, other: DateTimeFormat) -> str:
1541 if not isinstance(other, DateTimeFormat):
1542 return NotImplemented
1543 return self.format % other
1545 def apply(
1546 self,
1547 datetime: datetime.date | datetime.time,
1548 locale: Locale | str | None,
1549 reference_date: datetime.date | None = None,
1550 ) -> str:
1551 return self % DateTimeFormat(datetime, locale, reference_date)
1554class DateTimeFormat:
1555 def __init__(
1556 self,
1557 value: datetime.date | datetime.time,
1558 locale: Locale | str,
1559 reference_date: datetime.date | None = None,
1560 ) -> None:
1561 assert isinstance(value, (datetime.date, datetime.datetime, datetime.time))
1562 if isinstance(value, (datetime.datetime, datetime.time)) and value.tzinfo is None:
1563 value = value.replace(tzinfo=UTC)
1564 self.value = value
1565 self.locale = Locale.parse(locale)
1566 self.reference_date = reference_date
1568 def __getitem__(self, name: str) -> str:
1569 char = name[0]
1570 num = len(name)
1571 if char == 'G':
1572 return self.format_era(char, num)
1573 elif char in ('y', 'Y', 'u'):
1574 return self.format_year(char, num)
1575 elif char in ('Q', 'q'):
1576 return self.format_quarter(char, num)
1577 elif char in ('M', 'L'):
1578 return self.format_month(char, num)
1579 elif char in ('w', 'W'):
1580 return self.format_week(char, num)
1581 elif char == 'd':
1582 return self.format(self.value.day, num)
1583 elif char == 'D':
1584 return self.format_day_of_year(num)
1585 elif char == 'F':
1586 return self.format_day_of_week_in_month()
1587 elif char in ('E', 'e', 'c'):
1588 return self.format_weekday(char, num)
1589 elif char in ('a', 'b', 'B'):
1590 return self.format_period(char, num)
1591 elif char == 'h':
1592 if self.value.hour % 12 == 0:
1593 return self.format(12, num)
1594 else:
1595 return self.format(self.value.hour % 12, num)
1596 elif char == 'H':
1597 return self.format(self.value.hour, num)
1598 elif char == 'K':
1599 return self.format(self.value.hour % 12, num)
1600 elif char == 'k':
1601 if self.value.hour == 0:
1602 return self.format(24, num)
1603 else:
1604 return self.format(self.value.hour, num)
1605 elif char == 'm':
1606 return self.format(self.value.minute, num)
1607 elif char == 's':
1608 return self.format(self.value.second, num)
1609 elif char == 'S':
1610 return self.format_frac_seconds(num)
1611 elif char == 'A':
1612 return self.format_milliseconds_in_day(num)
1613 elif char in ('z', 'Z', 'v', 'V', 'x', 'X', 'O'):
1614 return self.format_timezone(char, num)
1615 else:
1616 raise KeyError(f"Unsupported date/time field {char!r}")
1618 def extract(self, char: str) -> int:
1619 char = str(char)[0]
1620 if char == 'y':
1621 return self.value.year
1622 elif char == 'M':
1623 return self.value.month
1624 elif char == 'd':
1625 return self.value.day
1626 elif char == 'H':
1627 return self.value.hour
1628 elif char == 'h':
1629 return self.value.hour % 12 or 12
1630 elif char == 'm':
1631 return self.value.minute
1632 elif char == 'a':
1633 return int(self.value.hour >= 12) # 0 for am, 1 for pm
1634 else:
1635 raise NotImplementedError(
1636 f"Not implemented: extracting {char!r} from {self.value!r}",
1637 )
1639 def format_era(self, char: str, num: int) -> str:
1640 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)]
1641 era = int(self.value.year >= 0)
1642 return get_era_names(width, self.locale)[era]
1644 def format_year(self, char: str, num: int) -> str:
1645 value = self.value.year
1646 if char.isupper():
1647 month = self.value.month
1648 if month == 1 and self.value.day < 7 and self.get_week_of_year() >= 52:
1649 value -= 1
1650 elif month == 12 and self.value.day > 25 and self.get_week_of_year() <= 2:
1651 value += 1
1652 year = self.format(value, num)
1653 if num == 2:
1654 year = year[-2:]
1655 return year
1657 def format_quarter(self, char: str, num: int) -> str:
1658 quarter = (self.value.month - 1) // 3 + 1
1659 if num <= 2:
1660 return '%0*d' % (num, quarter)
1661 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
1662 context = {'Q': 'format', 'q': 'stand-alone'}[char]
1663 return get_quarter_names(width, context, self.locale)[quarter]
1665 def format_month(self, char: str, num: int) -> str:
1666 if num <= 2:
1667 return '%0*d' % (num, self.value.month)
1668 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
1669 context = {'M': 'format', 'L': 'stand-alone'}[char]
1670 return get_month_names(width, context, self.locale)[self.value.month]
1672 def format_week(self, char: str, num: int) -> str:
1673 if char.islower(): # week of year
1674 week = self.get_week_of_year()
1675 return self.format(week, num)
1676 else: # week of month
1677 week = self.get_week_of_month()
1678 return str(week)
1680 def format_weekday(self, char: str = 'E', num: int = 4) -> str:
1681 """
1682 Return weekday from parsed datetime according to format pattern.
1684 >>> from datetime import date
1685 >>> format = DateTimeFormat(date(2016, 2, 28), Locale.parse('en_US'))
1686 >>> format.format_weekday()
1687 'Sunday'
1689 'E': Day of week - Use one through three letters for the abbreviated day name, four for the full (wide) name,
1690 five for the narrow name, or six for the short name.
1691 >>> format.format_weekday('E',2)
1692 'Sun'
1694 'e': Local day of week. Same as E except adds a numeric value that will depend on the local starting day of the
1695 week, using one or two letters. For this example, Monday is the first day of the week.
1696 >>> format.format_weekday('e',2)
1697 '01'
1699 'c': Stand-Alone local day of week - Use one letter for the local numeric value (same as 'e'), three for the
1700 abbreviated day name, four for the full (wide) name, five for the narrow name, or six for the short name.
1701 >>> format.format_weekday('c',1)
1702 '1'
1704 :param char: pattern format character ('e','E','c')
1705 :param num: count of format character
1707 """
1708 if num < 3:
1709 if char.islower():
1710 value = 7 - self.locale.first_week_day + self.value.weekday()
1711 return self.format(value % 7 + 1, num)
1712 num = 3
1713 weekday = self.value.weekday()
1714 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow', 6: 'short'}[num]
1715 context = "stand-alone" if char == "c" else "format"
1716 return get_day_names(width, context, self.locale)[weekday]
1718 def format_day_of_year(self, num: int) -> str:
1719 return self.format(self.get_day_of_year(), num)
1721 def format_day_of_week_in_month(self) -> str:
1722 return str((self.value.day - 1) // 7 + 1)
1724 def format_period(self, char: str, num: int) -> str:
1725 """
1726 Return period from parsed datetime according to format pattern.
1728 >>> from datetime import datetime, time
1729 >>> format = DateTimeFormat(time(13, 42), 'fi_FI')
1730 >>> format.format_period('a', 1)
1731 'ip.'
1732 >>> format.format_period('b', 1)
1733 'iltap.'
1734 >>> format.format_period('b', 4)
1735 'iltapäivä'
1736 >>> format.format_period('B', 4)
1737 'iltapäivällä'
1738 >>> format.format_period('B', 5)
1739 'ip.'
1741 >>> format = DateTimeFormat(datetime(2022, 4, 28, 6, 27), 'zh_Hant')
1742 >>> format.format_period('a', 1)
1743 '上午'
1744 >>> format.format_period('B', 1)
1745 '清晨'
1747 :param char: pattern format character ('a', 'b', 'B')
1748 :param num: count of format character
1750 """
1751 widths = [
1752 {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)],
1753 'wide',
1754 'narrow',
1755 'abbreviated',
1756 ]
1757 if char == 'a':
1758 period = 'pm' if self.value.hour >= 12 else 'am'
1759 context = 'format'
1760 else:
1761 period = get_period_id(self.value, locale=self.locale)
1762 context = 'format' if char == 'B' else 'stand-alone'
1763 for width in widths:
1764 period_names = get_period_names(context=context, width=width, locale=self.locale)
1765 if period in period_names:
1766 return period_names[period]
1767 raise ValueError(f"Could not format period {period} in {self.locale}")
1769 def format_frac_seconds(self, num: int) -> str:
1770 """ Return fractional seconds.
1772 Truncates the time's microseconds to the number of digits passed in,
1773 as specified by UTS #35.
1774 """
1775 micro = self.value.microsecond
1776 frac = micro // 10**(6 - num) if num <= 6 else micro * 10**(num - 6)
1777 return self.format(frac, num)
1779 def format_milliseconds_in_day(self, num):
1780 msecs = (
1781 self.value.microsecond // 1000
1782 + self.value.second * 1000
1783 + self.value.minute * 60000
1784 + self.value.hour * 3600000
1785 )
1786 return self.format(msecs, num)
1788 def format_timezone(self, char: str, num: int) -> str:
1789 width = {3: 'short', 4: 'long', 5: 'iso8601'}[max(3, num)]
1791 # It could be that we only receive a time to format, but also have a
1792 # reference date which is important to distinguish between timezone
1793 # variants (summer/standard time)
1794 value = self.value
1795 if self.reference_date:
1796 value = datetime.datetime.combine(self.reference_date, self.value)
1798 if char == 'z':
1799 return get_timezone_name(value, width, locale=self.locale)
1800 elif char == 'Z':
1801 if num == 5:
1802 return get_timezone_gmt(value, width, locale=self.locale, return_z=True)
1803 return get_timezone_gmt(value, width, locale=self.locale)
1804 elif char == 'O':
1805 if num == 4:
1806 return get_timezone_gmt(value, width, locale=self.locale)
1807 # TODO: To add support for O:1
1808 elif char == 'v':
1809 return get_timezone_name(value.tzinfo, width, locale=self.locale)
1810 elif char == 'V':
1811 if num == 1:
1812 return get_timezone_name(value.tzinfo, width, locale=self.locale)
1813 elif num == 2:
1814 return get_timezone_name(value.tzinfo, locale=self.locale, return_zone=True)
1815 elif num == 3:
1816 return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True) # fmt: skip
1817 return get_timezone_location(value.tzinfo, locale=self.locale)
1818 elif char in 'Xx':
1819 return_z = char == 'X'
1820 if num == 1:
1821 width = 'iso8601_short'
1822 elif num in (2, 4):
1823 width = 'short'
1824 elif num in (3, 5):
1825 width = 'iso8601'
1826 return get_timezone_gmt(value, width=width, locale=self.locale, return_z=return_z) # fmt: skip
1828 def format(self, value: SupportsInt, length: int) -> str:
1829 return '%0*d' % (length, value)
1831 def get_day_of_year(self, date: datetime.date | None = None) -> int:
1832 if date is None:
1833 date = self.value
1834 return (date - date.replace(month=1, day=1)).days + 1
1836 def get_week_of_year(self) -> int:
1837 """Return the week of the year."""
1838 day_of_year = self.get_day_of_year(self.value)
1839 week = self.get_week_number(day_of_year)
1840 if week == 0:
1841 date = datetime.date(self.value.year - 1, 12, 31)
1842 week = self.get_week_number(self.get_day_of_year(date), date.weekday())
1843 elif week > 52:
1844 weekday = datetime.date(self.value.year + 1, 1, 1).weekday()
1845 if (
1846 self.get_week_number(1, weekday) == 1
1847 and 32 - (weekday - self.locale.first_week_day) % 7 <= self.value.day
1848 ):
1849 week = 1
1850 return week
1852 def get_week_of_month(self) -> int:
1853 """Return the week of the month."""
1854 return self.get_week_number(self.value.day)
1856 def get_week_number(self, day_of_period: int, day_of_week: int | None = None) -> int:
1857 """Return the number of the week of a day within a period. This may be
1858 the week number in a year or the week number in a month.
1860 Usually this will return a value equal to or greater than 1, but if the
1861 first week of the period is so short that it actually counts as the last
1862 week of the previous period, this function will return 0.
1864 >>> date = datetime.date(2006, 1, 8)
1865 >>> DateTimeFormat(date, 'de_DE').get_week_number(6)
1866 1
1867 >>> DateTimeFormat(date, 'en_US').get_week_number(6)
1868 2
1870 :param day_of_period: the number of the day in the period (usually
1871 either the day of month or the day of year)
1872 :param day_of_week: the week day; if omitted, the week day of the
1873 current date is assumed
1874 """
1875 if day_of_week is None:
1876 day_of_week = self.value.weekday()
1877 first_day = (day_of_week - self.locale.first_week_day - day_of_period + 1) % 7
1878 if first_day < 0:
1879 first_day += 7
1880 week_number = (day_of_period + first_day - 1) // 7
1881 if 7 - first_day >= self.locale.min_week_days:
1882 week_number += 1
1883 return week_number
1886PATTERN_CHARS: dict[str, list[int] | None] = {
1887 'G': [1, 2, 3, 4, 5], # era
1888 'y': None, 'Y': None, 'u': None, # year
1889 'Q': [1, 2, 3, 4, 5], 'q': [1, 2, 3, 4, 5], # quarter
1890 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month
1891 'w': [1, 2], 'W': [1], # week
1892 'd': [1, 2, 3], 'D': [1, 2, 3], 'F': [1], 'g': None, # day
1893 'E': [1, 2, 3, 4, 5, 6], 'e': [1, 2, 3, 4, 5, 6], 'c': [1, 3, 4, 5, 6], # week day
1894 'a': [1, 2, 3, 4, 5], 'b': [1, 2, 3, 4, 5], 'B': [1, 2, 3, 4, 5], # period
1895 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour
1896 'm': [1, 2], # minute
1897 's': [1, 2], 'S': None, 'A': None, # second
1898 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4, 5], 'O': [1, 4], 'v': [1, 4], # zone
1899 'V': [1, 2, 3, 4], 'x': [1, 2, 3, 4, 5], 'X': [1, 2, 3, 4, 5], # zone
1900} # fmt: skip
1902#: The pattern characters declared in the Date Field Symbol Table
1903#: (https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
1904#: in order of decreasing magnitude.
1905PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx"
1908def parse_pattern(pattern: str | DateTimePattern) -> DateTimePattern:
1909 """Parse date, time, and datetime format patterns.
1911 >>> parse_pattern("MMMMd").format
1912 '%(MMMM)s%(d)s'
1913 >>> parse_pattern("MMM d, yyyy").format
1914 '%(MMM)s %(d)s, %(yyyy)s'
1916 Pattern can contain literal strings in single quotes:
1918 >>> parse_pattern("H:mm' Uhr 'z").format
1919 '%(H)s:%(mm)s Uhr %(z)s'
1921 An actual single quote can be used by using two adjacent single quote
1922 characters:
1924 >>> parse_pattern("hh' o''clock'").format
1925 "%(hh)s o'clock"
1927 :param pattern: the formatting pattern to parse
1928 """
1929 if isinstance(pattern, DateTimePattern):
1930 return pattern
1931 return _cached_parse_pattern(pattern)
1934@lru_cache(maxsize=1024)
1935def _cached_parse_pattern(pattern: str) -> DateTimePattern:
1936 result = []
1938 for tok_type, tok_value in tokenize_pattern(pattern):
1939 if tok_type == "chars":
1940 result.append(tok_value.replace('%', '%%'))
1941 elif tok_type == "field":
1942 fieldchar, fieldnum = tok_value
1943 limit = PATTERN_CHARS[fieldchar]
1944 if limit and fieldnum not in limit:
1945 raise ValueError(f"Invalid length for field: {fieldchar * fieldnum!r}")
1946 result.append('%%(%s)s' % (fieldchar * fieldnum))
1947 else:
1948 raise NotImplementedError(f"Unknown token type: {tok_type}")
1949 return DateTimePattern(pattern, ''.join(result))
1952def tokenize_pattern(pattern: str) -> list[tuple[str, str | tuple[str, int]]]:
1953 """
1954 Tokenize date format patterns.
1956 Returns a list of (token_type, token_value) tuples.
1958 ``token_type`` may be either "chars" or "field".
1960 For "chars" tokens, the value is the literal value.
1962 For "field" tokens, the value is a tuple of (field character, repetition count).
1964 :param pattern: Pattern string
1965 """
1966 result = []
1967 quotebuf = None
1968 charbuf = []
1969 fieldchar = ['']
1970 fieldnum = [0]
1972 def append_chars():
1973 result.append(('chars', ''.join(charbuf).replace('\0', "'")))
1974 del charbuf[:]
1976 def append_field():
1977 result.append(('field', (fieldchar[0], fieldnum[0])))
1978 fieldchar[0] = ''
1979 fieldnum[0] = 0
1981 for char in pattern.replace("''", '\0'):
1982 if quotebuf is None:
1983 if char == "'": # quote started
1984 if fieldchar[0]:
1985 append_field()
1986 elif charbuf:
1987 append_chars()
1988 quotebuf = []
1989 elif char in PATTERN_CHARS:
1990 if charbuf:
1991 append_chars()
1992 if char == fieldchar[0]:
1993 fieldnum[0] += 1
1994 else:
1995 if fieldchar[0]:
1996 append_field()
1997 fieldchar[0] = char
1998 fieldnum[0] = 1
1999 else:
2000 if fieldchar[0]:
2001 append_field()
2002 charbuf.append(char)
2004 elif quotebuf is not None:
2005 if char == "'": # end of quote
2006 charbuf.extend(quotebuf)
2007 quotebuf = None
2008 else: # inside quote
2009 quotebuf.append(char)
2011 if fieldchar[0]:
2012 append_field()
2013 elif charbuf:
2014 append_chars()
2016 return result
2019def untokenize_pattern(tokens: Iterable[tuple[str, str | tuple[str, int]]]) -> str:
2020 """
2021 Turn a date format pattern token stream back into a string.
2023 This is the reverse operation of ``tokenize_pattern``.
2024 """
2025 output = []
2026 for tok_type, tok_value in tokens:
2027 if tok_type == "field":
2028 output.append(tok_value[0] * tok_value[1])
2029 elif tok_type == "chars":
2030 if not any(ch in PATTERN_CHARS for ch in tok_value): # No need to quote
2031 output.append(tok_value)
2032 else:
2033 output.append("'%s'" % tok_value.replace("'", "''"))
2034 return "".join(output)
2037def split_interval_pattern(pattern: str) -> list[str]:
2038 """
2039 Split an interval-describing datetime pattern into multiple pieces.
2041 > The pattern is then designed to be broken up into two pieces by determining the first repeating field.
2042 - https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats
2044 >>> split_interval_pattern('E d.M. – E d.M.')
2045 ['E d.M. – ', 'E d.M.']
2046 >>> split_interval_pattern("Y 'text' Y 'more text'")
2047 ["Y 'text '", "Y 'more text'"]
2048 >>> split_interval_pattern('E, MMM d – E')
2049 ['E, MMM d – ', 'E']
2050 >>> split_interval_pattern("MMM d")
2051 ['MMM d']
2052 >>> split_interval_pattern("y G")
2053 ['y G']
2054 >>> split_interval_pattern('MMM d – d')
2055 ['MMM d – ', 'd']
2057 :param pattern: Interval pattern string
2058 :return: list of "subpatterns"
2059 """
2061 seen_fields = set()
2062 parts = [[]]
2064 for tok_type, tok_value in tokenize_pattern(pattern):
2065 if tok_type == "field":
2066 if tok_value[0] in seen_fields: # Repeated field
2067 parts.append([])
2068 seen_fields.clear()
2069 seen_fields.add(tok_value[0])
2070 parts[-1].append((tok_type, tok_value))
2072 return [untokenize_pattern(tokens) for tokens in parts]
2075def match_skeleton(
2076 skeleton: str,
2077 options: Iterable[str],
2078 allow_different_fields: bool = False,
2079) -> str | None:
2080 """
2081 Find the closest match for the given datetime skeleton among the options given.
2083 This uses the rules outlined in the TR35 document.
2085 >>> match_skeleton('yMMd', ('yMd', 'yMMMd'))
2086 'yMd'
2088 >>> match_skeleton('yMMd', ('jyMMd',), allow_different_fields=True)
2089 'jyMMd'
2091 >>> match_skeleton('yMMd', ('qyMMd',), allow_different_fields=False)
2093 >>> match_skeleton('hmz', ('hmv',))
2094 'hmv'
2096 :param skeleton: The skeleton to match
2097 :param options: An iterable of other skeletons to match against
2098 :param allow_different_fields: Whether to allow a match that uses different fields
2099 than the skeleton requested.
2100 :return: The closest skeleton match, or if no match was found, None.
2101 :rtype: str|None
2102 """
2104 # TODO: maybe implement pattern expansion?
2106 # Based on the implementation in
2107 # https://github.com/unicode-org/icu/blob/main/icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalInfo.java
2109 # Filter out falsy values and sort for stability; when `interval_formats` is passed in, there may be a None key.
2110 options = sorted(option for option in options if option)
2112 if 'z' in skeleton and not any('z' in option for option in options):
2113 skeleton = skeleton.replace('z', 'v')
2114 if 'k' in skeleton and not any('k' in option for option in options):
2115 skeleton = skeleton.replace('k', 'H')
2116 if 'K' in skeleton and not any('K' in option for option in options):
2117 skeleton = skeleton.replace('K', 'h')
2118 if 'a' in skeleton and not any('a' in option for option in options):
2119 skeleton = skeleton.replace('a', '')
2120 if 'b' in skeleton and not any('b' in option for option in options):
2121 skeleton = skeleton.replace('b', '')
2123 get_input_field_width = dict(t[1] for t in tokenize_pattern(skeleton) if t[0] == "field").get # fmt: skip
2124 best_skeleton = None
2125 best_distance = None
2126 for option in options:
2127 get_opt_field_width = dict(t[1] for t in tokenize_pattern(option) if t[0] == "field").get # fmt: skip
2128 distance = 0
2129 for field in PATTERN_CHARS:
2130 input_width = get_input_field_width(field, 0)
2131 opt_width = get_opt_field_width(field, 0)
2132 if input_width == opt_width:
2133 continue
2134 if opt_width == 0 or input_width == 0:
2135 if not allow_different_fields: # This one is not okay
2136 option = None
2137 break
2138 # Magic weight constant for "entirely different fields"
2139 distance += 0x1000
2140 elif field == 'M' and (
2141 (input_width > 2 and opt_width <= 2) or (input_width <= 2 and opt_width > 2)
2142 ):
2143 # Magic weight constant for "text turns into a number"
2144 distance += 0x100
2145 else:
2146 distance += abs(input_width - opt_width)
2148 if not option:
2149 # We lost the option along the way (probably due to "allow_different_fields")
2150 continue
2152 if not best_skeleton or distance < best_distance:
2153 best_skeleton = option
2154 best_distance = distance
2156 if distance == 0: # Found a perfect match!
2157 break
2159 return best_skeleton