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-2025 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 :type instant: date|time|datetime|int|float|None
150 :return: a datetime
151 :rtype: datetime
152 """
153 if instant is None:
154 return datetime.datetime.now(UTC).replace(tzinfo=None)
155 elif isinstance(instant, (int, float)):
156 return datetime.datetime.fromtimestamp(instant, UTC).replace(tzinfo=None)
157 elif isinstance(instant, datetime.time):
158 return datetime.datetime.combine(datetime.date.today(), instant)
159 elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): # fmt: skip
160 return datetime.datetime.combine(instant, datetime.time())
161 # TODO (3.x): Add an assertion/type check for this fallthrough branch:
162 return instant
165def _ensure_datetime_tzinfo(
166 dt: datetime.datetime,
167 tzinfo: datetime.tzinfo | None = None,
168) -> datetime.datetime:
169 """
170 Ensure the datetime passed has an attached tzinfo.
172 If the datetime is tz-naive to begin with, UTC is attached.
174 If a tzinfo is passed in, the datetime is normalized to that timezone.
176 >>> from datetime import datetime
177 >>> _get_tz_name(_ensure_datetime_tzinfo(datetime(2015, 1, 1)))
178 'UTC'
180 >>> tz = get_timezone("Europe/Stockholm")
181 >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1, 13, 15, tzinfo=UTC), tzinfo=tz).hour
182 14
184 :param datetime: Datetime to augment.
185 :param tzinfo: optional tzinfo
186 :return: datetime with tzinfo
187 :rtype: datetime
188 """
189 if dt.tzinfo is None:
190 dt = dt.replace(tzinfo=UTC)
191 if tzinfo is not None:
192 dt = dt.astimezone(get_timezone(tzinfo))
193 if hasattr(tzinfo, 'normalize'): # pytz
194 dt = tzinfo.normalize(dt)
195 return dt
198def _get_time(
199 time: datetime.time | datetime.datetime | None,
200 tzinfo: datetime.tzinfo | None = None,
201) -> datetime.time:
202 """
203 Get a timezoned time from a given instant.
205 .. warning:: The return values of this function may depend on the system clock.
207 :param time: time, datetime or None
208 :rtype: time
209 """
210 if time is None:
211 time = datetime.datetime.now(UTC)
212 elif isinstance(time, (int, float)):
213 time = datetime.datetime.fromtimestamp(time, UTC)
215 if time.tzinfo is None:
216 time = time.replace(tzinfo=UTC)
218 if isinstance(time, datetime.datetime):
219 if tzinfo is not None:
220 time = time.astimezone(tzinfo)
221 if hasattr(tzinfo, 'normalize'): # pytz
222 time = tzinfo.normalize(time)
223 time = time.timetz()
224 elif tzinfo is not None:
225 time = time.replace(tzinfo=tzinfo)
226 return time
229def get_timezone(zone: str | datetime.tzinfo | None = None) -> datetime.tzinfo:
230 """Looks up a timezone by name and returns it. The timezone object
231 returned comes from ``pytz`` or ``zoneinfo``, whichever is available.
232 It corresponds to the `tzinfo` interface and can be used with all of
233 the functions of Babel that operate with dates.
235 If a timezone is not known a :exc:`LookupError` is raised. If `zone`
236 is ``None`` a local zone object is returned.
238 :param zone: the name of the timezone to look up. If a timezone object
239 itself is passed in, it's returned unchanged.
240 """
241 if zone is None:
242 return LOCALTZ
243 if not isinstance(zone, str):
244 return zone
246 if pytz:
247 try:
248 return pytz.timezone(zone)
249 except pytz.UnknownTimeZoneError as e:
250 exc = e
251 else:
252 assert zoneinfo
253 try:
254 return zoneinfo.ZoneInfo(zone)
255 except zoneinfo.ZoneInfoNotFoundError as e:
256 exc = e
258 raise LookupError(f"Unknown timezone {zone}") from exc
261def get_period_names(
262 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
263 context: _Context = 'stand-alone',
264 locale: Locale | str | None = None,
265) -> LocaleDataDict:
266 """Return the names for day periods (AM/PM) used by the locale.
268 >>> get_period_names(locale='en_US')['am']
269 'AM'
271 :param width: the width to use, one of "abbreviated", "narrow", or "wide"
272 :param context: the context, either "format" or "stand-alone"
273 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
274 """
275 return Locale.parse(locale or LC_TIME).day_periods[context][width]
278def get_day_names(
279 width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide',
280 context: _Context = 'format',
281 locale: Locale | str | None = None,
282) -> LocaleDataDict:
283 """Return the day names used by the locale for the specified format.
285 >>> get_day_names('wide', locale='en_US')[1]
286 'Tuesday'
287 >>> get_day_names('short', locale='en_US')[1]
288 'Tu'
289 >>> get_day_names('abbreviated', locale='es')[1]
290 'mar'
291 >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1]
292 'D'
294 :param width: the width to use, one of "wide", "abbreviated", "short" or "narrow"
295 :param context: the context, either "format" or "stand-alone"
296 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
297 """
298 return Locale.parse(locale or LC_TIME).days[context][width]
301def get_month_names(
302 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
303 context: _Context = 'format',
304 locale: Locale | str | None = None,
305) -> LocaleDataDict:
306 """Return the month names used by the locale for the specified format.
308 >>> get_month_names('wide', locale='en_US')[1]
309 'January'
310 >>> get_month_names('abbreviated', locale='es')[1]
311 'ene'
312 >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1]
313 'J'
315 :param width: the width to use, one of "wide", "abbreviated", or "narrow"
316 :param context: the context, either "format" or "stand-alone"
317 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
318 """
319 return Locale.parse(locale or LC_TIME).months[context][width]
322def get_quarter_names(
323 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
324 context: _Context = 'format',
325 locale: Locale | str | None = None,
326) -> LocaleDataDict:
327 """Return the quarter names used by the locale for the specified format.
329 >>> get_quarter_names('wide', locale='en_US')[1]
330 '1st quarter'
331 >>> get_quarter_names('abbreviated', locale='de_DE')[1]
332 'Q1'
333 >>> get_quarter_names('narrow', locale='de_DE')[1]
334 '1'
336 :param width: the width to use, one of "wide", "abbreviated", or "narrow"
337 :param context: the context, either "format" or "stand-alone"
338 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
339 """
340 return Locale.parse(locale or LC_TIME).quarters[context][width]
343def get_era_names(
344 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
345 locale: Locale | str | None = None,
346) -> LocaleDataDict:
347 """Return the era names used by the locale for the specified format.
349 >>> get_era_names('wide', locale='en_US')[1]
350 'Anno Domini'
351 >>> get_era_names('abbreviated', locale='de_DE')[1]
352 'n. Chr.'
354 :param width: the width to use, either "wide", "abbreviated", or "narrow"
355 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
356 """
357 return Locale.parse(locale or LC_TIME).eras[width]
360def get_date_format(
361 format: _PredefinedTimeFormat = 'medium',
362 locale: Locale | str | None = None,
363) -> DateTimePattern:
364 """Return the date formatting patterns used by the locale for the specified
365 format.
367 >>> get_date_format(locale='en_US')
368 <DateTimePattern 'MMM d, y'>
369 >>> get_date_format('full', locale='de_DE')
370 <DateTimePattern 'EEEE, d. MMMM y'>
372 :param format: the format to use, one of "full", "long", "medium", or
373 "short"
374 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
375 """
376 return Locale.parse(locale or LC_TIME).date_formats[format]
379def get_datetime_format(
380 format: _PredefinedTimeFormat = 'medium',
381 locale: Locale | str | None = None,
382) -> DateTimePattern:
383 """Return the datetime formatting patterns used by the locale for the
384 specified format.
386 >>> get_datetime_format(locale='en_US')
387 '{1}, {0}'
389 :param format: the format to use, one of "full", "long", "medium", or
390 "short"
391 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
392 """
393 patterns = Locale.parse(locale or LC_TIME).datetime_formats
394 if format not in patterns:
395 format = None
396 return patterns[format]
399def get_time_format(
400 format: _PredefinedTimeFormat = 'medium',
401 locale: Locale | str | None = None,
402) -> DateTimePattern:
403 """Return the time formatting patterns used by the locale for the specified
404 format.
406 >>> get_time_format(locale='en_US')
407 <DateTimePattern 'h:mm:ss\\u202fa'>
408 >>> get_time_format('full', locale='de_DE')
409 <DateTimePattern 'HH:mm:ss zzzz'>
411 :param format: the format to use, one of "full", "long", "medium", or
412 "short"
413 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
414 """
415 return Locale.parse(locale or LC_TIME).time_formats[format]
418def get_timezone_gmt(
419 datetime: _Instant = None,
420 width: Literal['long', 'short', 'iso8601', 'iso8601_short'] = 'long',
421 locale: Locale | str | None = None,
422 return_z: bool = False,
423) -> str:
424 """Return the timezone associated with the given `datetime` object formatted
425 as string indicating the offset from GMT.
427 >>> from datetime import datetime
428 >>> dt = datetime(2007, 4, 1, 15, 30)
429 >>> get_timezone_gmt(dt, locale='en')
430 'GMT+00:00'
431 >>> get_timezone_gmt(dt, locale='en', return_z=True)
432 'Z'
433 >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
434 '+00'
435 >>> tz = get_timezone('America/Los_Angeles')
436 >>> dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
437 >>> get_timezone_gmt(dt, locale='en')
438 'GMT-07:00'
439 >>> get_timezone_gmt(dt, 'short', locale='en')
440 '-0700'
441 >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
442 '-07'
444 The long format depends on the locale, for example in France the acronym
445 UTC string is used instead of GMT:
447 >>> get_timezone_gmt(dt, 'long', locale='fr_FR')
448 'UTC-07:00'
450 .. versionadded:: 0.9
452 :param datetime: the ``datetime`` object; if `None`, the current date and
453 time in UTC is used
454 :param width: either "long" or "short" or "iso8601" or "iso8601_short"
455 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
456 :param return_z: True or False; Function returns indicator "Z"
457 when local time offset is 0
458 """
459 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime))
460 locale = Locale.parse(locale or LC_TIME)
462 offset = datetime.tzinfo.utcoffset(datetime)
463 seconds = offset.days * 24 * 60 * 60 + offset.seconds
464 hours, seconds = divmod(seconds, 3600)
465 if return_z and hours == 0 and seconds == 0:
466 return 'Z'
467 elif seconds == 0 and width == 'iso8601_short':
468 return '%+03d' % hours
469 elif width == 'short' or width == 'iso8601_short':
470 pattern = '%+03d%02d'
471 elif width == 'iso8601':
472 pattern = '%+03d:%02d'
473 else:
474 pattern = locale.zone_formats['gmt'] % '%+03d:%02d'
475 return pattern % (hours, seconds // 60)
478def get_timezone_location(
479 dt_or_tzinfo: _DtOrTzinfo = None,
480 locale: Locale | str | None = None,
481 return_city: bool = False,
482) -> str:
483 """Return a representation of the given timezone using "location format".
485 The result depends on both the local display name of the country and the
486 city associated with the time zone:
488 >>> tz = get_timezone('America/St_Johns')
489 >>> print(get_timezone_location(tz, locale='de_DE'))
490 Kanada (St. John’s) (Ortszeit)
491 >>> print(get_timezone_location(tz, locale='en'))
492 Canada (St. John’s) Time
493 >>> print(get_timezone_location(tz, locale='en', return_city=True))
494 St. John’s
495 >>> tz = get_timezone('America/Mexico_City')
496 >>> get_timezone_location(tz, locale='de_DE')
497 'Mexiko (Mexiko-Stadt) (Ortszeit)'
499 If the timezone is associated with a country that uses only a single
500 timezone, just the localized country name is returned:
502 >>> tz = get_timezone('Europe/Berlin')
503 >>> get_timezone_name(tz, locale='de_DE')
504 'Mitteleuropäische Zeit'
506 .. versionadded:: 0.9
508 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines
509 the timezone; if `None`, the current date and time in
510 UTC is assumed
511 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
512 :param return_city: True or False, if True then return exemplar city (location)
513 for the time zone
514 :return: the localized timezone name using location format
516 """
517 locale = Locale.parse(locale or LC_TIME)
519 zone = _get_tz_name(dt_or_tzinfo)
521 # Get the canonical time-zone code
522 zone = get_global('zone_aliases').get(zone, zone)
524 info = locale.time_zones.get(zone, {})
526 # Otherwise, if there is only one timezone for the country, return the
527 # localized country name
528 region_format = locale.zone_formats['region']
529 territory = get_global('zone_territories').get(zone)
530 if territory not in locale.territories:
531 territory = 'ZZ' # invalid/unknown
532 territory_name = locale.territories[territory]
533 if (
534 not return_city
535 and territory
536 and len(get_global('territory_zones').get(territory, [])) == 1
537 ):
538 return region_format % territory_name
540 # Otherwise, include the city in the output
541 fallback_format = locale.zone_formats['fallback']
542 if 'city' in info:
543 city_name = info['city']
544 else:
545 metazone = get_global('meta_zones').get(zone)
546 metazone_info = locale.meta_zones.get(metazone, {})
547 if 'city' in metazone_info:
548 city_name = metazone_info['city']
549 elif '/' in zone:
550 city_name = zone.split('/', 1)[1].replace('_', ' ')
551 else:
552 city_name = zone.replace('_', ' ')
554 if return_city:
555 return city_name
556 return region_format % (
557 fallback_format
558 % {
559 '0': city_name,
560 '1': territory_name,
561 }
562 )
565def get_timezone_name(
566 dt_or_tzinfo: _DtOrTzinfo = None,
567 width: Literal['long', 'short'] = 'long',
568 uncommon: bool = False,
569 locale: Locale | str | None = None,
570 zone_variant: Literal['generic', 'daylight', 'standard'] | None = None,
571 return_zone: bool = False,
572) -> str:
573 r"""Return the localized display name for the given timezone. The timezone
574 may be specified using a ``datetime`` or `tzinfo` object.
576 >>> from datetime import time
577 >>> dt = time(15, 30, tzinfo=get_timezone('America/Los_Angeles'))
578 >>> get_timezone_name(dt, locale='en_US') # doctest: +SKIP
579 'Pacific Standard Time'
580 >>> get_timezone_name(dt, locale='en_US', return_zone=True)
581 'America/Los_Angeles'
582 >>> get_timezone_name(dt, width='short', locale='en_US') # doctest: +SKIP
583 'PST'
585 If this function gets passed only a `tzinfo` object and no concrete
586 `datetime`, the returned display name is independent of daylight savings
587 time. This can be used for example for selecting timezones, or to set the
588 time of events that recur across DST changes:
590 >>> tz = get_timezone('America/Los_Angeles')
591 >>> get_timezone_name(tz, locale='en_US')
592 'Pacific Time'
593 >>> get_timezone_name(tz, 'short', locale='en_US')
594 'PT'
596 If no localized display name for the timezone is available, and the timezone
597 is associated with a country that uses only a single timezone, the name of
598 that country is returned, formatted according to the locale:
600 >>> tz = get_timezone('Europe/Berlin')
601 >>> get_timezone_name(tz, locale='de_DE')
602 'Mitteleuropäische Zeit'
603 >>> get_timezone_name(tz, locale='pt_BR')
604 'Horário da Europa Central'
606 On the other hand, if the country uses multiple timezones, the city is also
607 included in the representation:
609 >>> tz = get_timezone('America/St_Johns')
610 >>> get_timezone_name(tz, locale='de_DE')
611 'Neufundland-Zeit'
613 Note that short format is currently not supported for all timezones and
614 all locales. This is partially because not every timezone has a short
615 code in every locale. In that case it currently falls back to the long
616 format.
618 For more information see `LDML Appendix J: Time Zone Display Names
619 <https://www.unicode.org/reports/tr35/#Time_Zone_Fallback>`_
621 .. versionadded:: 0.9
623 .. versionchanged:: 1.0
624 Added `zone_variant` support.
626 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines
627 the timezone; if a ``tzinfo`` object is used, the
628 resulting display name will be generic, i.e.
629 independent of daylight savings time; if `None`, the
630 current date in UTC is assumed
631 :param width: either "long" or "short"
632 :param uncommon: deprecated and ignored
633 :param zone_variant: defines the zone variation to return. By default the
634 variation is defined from the datetime object
635 passed in. If no datetime object is passed in, the
636 ``'generic'`` variation is assumed. The following
637 values are valid: ``'generic'``, ``'daylight'`` and
638 ``'standard'``.
639 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
640 :param return_zone: True or False. If true then function
641 returns long time zone ID
642 """
643 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
644 locale = Locale.parse(locale or LC_TIME)
646 zone = _get_tz_name(dt_or_tzinfo)
648 if zone_variant is None:
649 if dt is None:
650 zone_variant = 'generic'
651 else:
652 dst = tzinfo.dst(dt)
653 zone_variant = "daylight" if dst else "standard"
654 else:
655 if zone_variant not in ('generic', 'standard', 'daylight'):
656 raise ValueError('Invalid zone variation')
658 # Get the canonical time-zone code
659 zone = get_global('zone_aliases').get(zone, zone)
660 if return_zone:
661 return zone
662 info = locale.time_zones.get(zone, {})
663 # Try explicitly translated zone names first
664 if width in info and zone_variant in info[width]:
665 value = info[width][zone_variant]
666 if value != NO_INHERITANCE_MARKER:
667 return value
669 metazone = get_global('meta_zones').get(zone)
670 if metazone:
671 metazone_info = locale.meta_zones.get(metazone, {})
672 if width in metazone_info:
673 name = metazone_info[width].get(zone_variant)
674 if width == 'short' and name == NO_INHERITANCE_MARKER:
675 # If the short form is marked no-inheritance,
676 # try to fall back to the long name instead.
677 name = metazone_info.get('long', {}).get(zone_variant)
678 if name and name != NO_INHERITANCE_MARKER:
679 return name
681 # If we have a concrete datetime, we assume that the result can't be
682 # independent of daylight savings time, so we return the GMT offset
683 if dt is not None:
684 return get_timezone_gmt(dt, width=width, locale=locale)
686 return get_timezone_location(dt_or_tzinfo, locale=locale)
689def format_date(
690 date: datetime.date | None = None,
691 format: _PredefinedTimeFormat | str = 'medium',
692 locale: Locale | str | None = None,
693) -> str:
694 """Return a date formatted according to the given pattern.
696 >>> from datetime import date
697 >>> d = date(2007, 4, 1)
698 >>> format_date(d, locale='en_US')
699 'Apr 1, 2007'
700 >>> format_date(d, format='full', locale='de_DE')
701 'Sonntag, 1. April 2007'
703 If you don't want to use the locale default formats, you can specify a
704 custom date pattern:
706 >>> format_date(d, "EEE, MMM d, ''yy", locale='en')
707 "Sun, Apr 1, '07"
709 :param date: the ``date`` or ``datetime`` object; if `None`, the current
710 date is used
711 :param format: one of "full", "long", "medium", or "short", or a custom
712 date/time pattern
713 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
714 """
715 if date is None:
716 date = datetime.date.today()
717 elif isinstance(date, datetime.datetime):
718 date = date.date()
720 locale = Locale.parse(locale or LC_TIME)
721 if format in ('full', 'long', 'medium', 'short'):
722 format = get_date_format(format, locale=locale)
723 pattern = parse_pattern(format)
724 return pattern.apply(date, locale)
727def format_datetime(
728 datetime: _Instant = None,
729 format: _PredefinedTimeFormat | str = 'medium',
730 tzinfo: datetime.tzinfo | None = None,
731 locale: Locale | str | None = None,
732) -> str:
733 r"""Return a date formatted according to the given pattern.
735 >>> from datetime import datetime
736 >>> dt = datetime(2007, 4, 1, 15, 30)
737 >>> format_datetime(dt, locale='en_US')
738 'Apr 1, 2007, 3:30:00\u202fPM'
740 For any pattern requiring the display of the timezone:
742 >>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'),
743 ... locale='fr_FR')
744 'dimanche 1 avril 2007, 17:30:00 heure d’été d’Europe centrale'
745 >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
746 ... tzinfo=get_timezone('US/Eastern'), locale='en')
747 '2007.04.01 AD at 11:30:00 EDT'
749 :param datetime: the `datetime` object; if `None`, the current date and
750 time is used
751 :param format: one of "full", "long", "medium", or "short", or a custom
752 date/time pattern
753 :param tzinfo: the timezone to apply to the time for display
754 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
755 """
756 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime), tzinfo)
758 locale = Locale.parse(locale or LC_TIME)
759 if format in ('full', 'long', 'medium', 'short'):
760 return (
761 get_datetime_format(format, locale=locale)
762 .replace("'", "")
763 .replace('{0}', format_time(datetime, format, tzinfo=None, locale=locale))
764 .replace('{1}', format_date(datetime, format, locale=locale))
765 )
766 else:
767 return parse_pattern(format).apply(datetime, locale)
770def format_time(
771 time: datetime.time | datetime.datetime | float | None = None,
772 format: _PredefinedTimeFormat | str = 'medium',
773 tzinfo: datetime.tzinfo | None = None,
774 locale: Locale | str | None = None,
775) -> str:
776 r"""Return a time formatted according to the given pattern.
778 >>> from datetime import datetime, time
779 >>> t = time(15, 30)
780 >>> format_time(t, locale='en_US')
781 '3:30:00\u202fPM'
782 >>> format_time(t, format='short', locale='de_DE')
783 '15:30'
785 If you don't want to use the locale default formats, you can specify a
786 custom time pattern:
788 >>> format_time(t, "hh 'o''clock' a", locale='en')
789 "03 o'clock PM"
791 For any pattern requiring the display of the time-zone a
792 timezone has to be specified explicitly:
794 >>> t = datetime(2007, 4, 1, 15, 30)
795 >>> tzinfo = get_timezone('Europe/Paris')
796 >>> t = _localize(tzinfo, t)
797 >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR')
798 '15:30:00 heure d’été d’Europe centrale'
799 >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'),
800 ... locale='en')
801 "09 o'clock AM, Eastern Daylight Time"
803 As that example shows, when this function gets passed a
804 ``datetime.datetime`` value, the actual time in the formatted string is
805 adjusted to the timezone specified by the `tzinfo` parameter. If the
806 ``datetime`` is "naive" (i.e. it has no associated timezone information),
807 it is assumed to be in UTC.
809 These timezone calculations are **not** performed if the value is of type
810 ``datetime.time``, as without date information there's no way to determine
811 what a given time would translate to in a different timezone without
812 information about whether daylight savings time is in effect or not. This
813 means that time values are left as-is, and the value of the `tzinfo`
814 parameter is only used to display the timezone name if needed:
816 >>> t = time(15, 30)
817 >>> format_time(t, format='full', tzinfo=get_timezone('Europe/Paris'),
818 ... locale='fr_FR') # doctest: +SKIP
819 '15:30:00 heure normale d\u2019Europe centrale'
820 >>> format_time(t, format='full', tzinfo=get_timezone('US/Eastern'),
821 ... locale='en_US') # doctest: +SKIP
822 '3:30:00\u202fPM Eastern Standard Time'
824 :param time: the ``time`` or ``datetime`` object; if `None`, the current
825 time in UTC is used
826 :param format: one of "full", "long", "medium", or "short", or a custom
827 date/time pattern
828 :param tzinfo: the time-zone to apply to the time for display
829 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
830 """
832 # get reference date for if we need to find the right timezone variant
833 # in the pattern
834 ref_date = time.date() if isinstance(time, datetime.datetime) else None
836 time = _get_time(time, tzinfo)
838 locale = Locale.parse(locale or LC_TIME)
839 if format in ('full', 'long', 'medium', 'short'):
840 format = get_time_format(format, locale=locale)
841 return parse_pattern(format).apply(time, locale, reference_date=ref_date)
844def format_skeleton(
845 skeleton: str,
846 datetime: _Instant = None,
847 tzinfo: datetime.tzinfo | None = None,
848 fuzzy: bool = True,
849 locale: Locale | str | None = None,
850) -> str:
851 r"""Return a time and/or date formatted according to the given pattern.
853 The skeletons are defined in the CLDR data and provide more flexibility
854 than the simple short/long/medium formats, but are a bit harder to use.
855 The are defined using the date/time symbols without order or punctuation
856 and map to a suitable format for the given locale.
858 >>> from datetime import datetime
859 >>> t = datetime(2007, 4, 1, 15, 30)
860 >>> format_skeleton('MMMEd', t, locale='fr')
861 'dim. 1 avr.'
862 >>> format_skeleton('MMMEd', t, locale='en')
863 'Sun, Apr 1'
864 >>> format_skeleton('yMMd', t, locale='fi') # yMMd is not in the Finnish locale; yMd gets used
865 '1.4.2007'
866 >>> format_skeleton('yMMd', t, fuzzy=False, locale='fi') # yMMd is not in the Finnish locale, an error is thrown
867 Traceback (most recent call last):
868 ...
869 KeyError: yMMd
870 >>> 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
871 Traceback (most recent call last):
872 ...
873 KeyError: None
875 After the skeleton is resolved to a pattern `format_datetime` is called so
876 all timezone processing etc is the same as for that.
878 :param skeleton: A date time skeleton as defined in the cldr data.
879 :param datetime: the ``time`` or ``datetime`` object; if `None`, the current
880 time in UTC is used
881 :param tzinfo: the time-zone to apply to the time for display
882 :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's
883 close enough to it. If there is no close match, a `KeyError`
884 is thrown.
885 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
886 """
887 locale = Locale.parse(locale or LC_TIME)
888 if fuzzy and skeleton not in locale.datetime_skeletons:
889 skeleton = match_skeleton(skeleton, locale.datetime_skeletons)
890 format = locale.datetime_skeletons[skeleton]
891 return format_datetime(datetime, format, tzinfo, locale)
894TIMEDELTA_UNITS: tuple[tuple[str, int], ...] = (
895 ('year', 3600 * 24 * 365),
896 ('month', 3600 * 24 * 30),
897 ('week', 3600 * 24 * 7),
898 ('day', 3600 * 24),
899 ('hour', 3600),
900 ('minute', 60),
901 ('second', 1),
902)
905def format_timedelta(
906 delta: datetime.timedelta | int,
907 granularity: Literal[
908 'year',
909 'month',
910 'week',
911 'day',
912 'hour',
913 'minute',
914 'second',
915 ] = 'second',
916 threshold: float = 0.85,
917 add_direction: bool = False,
918 format: Literal['narrow', 'short', 'medium', 'long'] = 'long',
919 locale: Locale | str | None = None,
920) -> str:
921 """Return a time delta according to the rules of the given locale.
923 >>> from datetime import timedelta
924 >>> format_timedelta(timedelta(weeks=12), locale='en_US')
925 '3 months'
926 >>> format_timedelta(timedelta(seconds=1), locale='es')
927 '1 segundo'
929 The granularity parameter can be provided to alter the lowest unit
930 presented, which defaults to a second.
932 >>> format_timedelta(timedelta(hours=3), granularity='day', locale='en_US')
933 '1 day'
935 The threshold parameter can be used to determine at which value the
936 presentation switches to the next higher unit. A higher threshold factor
937 means the presentation will switch later. For example:
939 >>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US')
940 '1 day'
941 >>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US')
942 '23 hours'
944 In addition directional information can be provided that informs
945 the user if the date is in the past or in the future:
947 >>> format_timedelta(timedelta(hours=1), add_direction=True, locale='en')
948 'in 1 hour'
949 >>> format_timedelta(timedelta(hours=-1), add_direction=True, locale='en')
950 '1 hour ago'
952 The format parameter controls how compact or wide the presentation is:
954 >>> format_timedelta(timedelta(hours=3), format='short', locale='en')
955 '3 hr'
956 >>> format_timedelta(timedelta(hours=3), format='narrow', locale='en')
957 '3h'
959 :param delta: a ``timedelta`` object representing the time difference to
960 format, or the delta in seconds as an `int` value
961 :param granularity: determines the smallest unit that should be displayed,
962 the value can be one of "year", "month", "week", "day",
963 "hour", "minute" or "second"
964 :param threshold: factor that determines at which point the presentation
965 switches to the next higher unit
966 :param add_direction: if this flag is set to `True` the return value will
967 include directional information. For instance a
968 positive timedelta will include the information about
969 it being in the future, a negative will be information
970 about the value being in the past.
971 :param format: the format, can be "narrow", "short" or "long". (
972 "medium" is deprecated, currently converted to "long" to
973 maintain compatibility)
974 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
975 """
976 if format not in ('narrow', 'short', 'medium', 'long'):
977 raise TypeError('Format must be one of "narrow", "short" or "long"')
978 if format == 'medium':
979 warnings.warn(
980 '"medium" value for format param of format_timedelta is deprecated. Use "long" instead',
981 category=DeprecationWarning,
982 stacklevel=2,
983 )
984 format = 'long'
985 if isinstance(delta, datetime.timedelta):
986 seconds = int((delta.days * 86400) + delta.seconds)
987 else:
988 seconds = delta
989 locale = Locale.parse(locale or LC_TIME)
990 date_fields = locale._data["date_fields"]
991 unit_patterns = locale._data["unit_patterns"]
993 def _iter_patterns(a_unit):
994 if add_direction:
995 # Try to find the length variant version first ("year-narrow")
996 # before falling back to the default.
997 unit_rel_patterns = date_fields.get(f"{a_unit}-{format}") or date_fields[a_unit]
998 if seconds >= 0:
999 yield unit_rel_patterns['future']
1000 else:
1001 yield unit_rel_patterns['past']
1002 a_unit = f"duration-{a_unit}"
1003 unit_pats = unit_patterns.get(a_unit, {})
1004 yield unit_pats.get(format)
1005 # We do not support `<alias>` tags at all while ingesting CLDR data,
1006 # so these aliases specified in `root.xml` are hard-coded here:
1007 # <unitLength type="long"><alias source="locale" path="../unitLength[@type='short']"/></unitLength>
1008 # <unitLength type="narrow"><alias source="locale" path="../unitLength[@type='short']"/></unitLength>
1009 if format in ("long", "narrow"):
1010 yield unit_pats.get("short")
1012 for unit, secs_per_unit in TIMEDELTA_UNITS:
1013 value = abs(seconds) / secs_per_unit
1014 if value >= threshold or unit == granularity:
1015 if unit == granularity and value > 0:
1016 value = max(1, value)
1017 value = int(round(value))
1018 plural_form = locale.plural_form(value)
1019 pattern = None
1020 for patterns in _iter_patterns(unit):
1021 if patterns is not None:
1022 pattern = patterns.get(plural_form) or patterns.get('other')
1023 if pattern:
1024 break
1025 # This really should not happen
1026 if pattern is None:
1027 return ''
1028 return pattern.replace('{0}', str(value))
1030 return ''
1033def _format_fallback_interval(
1034 start: _Instant,
1035 end: _Instant,
1036 skeleton: str | None,
1037 tzinfo: datetime.tzinfo | None,
1038 locale: Locale,
1039) -> str:
1040 if skeleton in locale.datetime_skeletons: # Use the given skeleton
1041 format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale)
1042 elif all(
1043 # Both are just dates
1044 (isinstance(d, datetime.date) and not isinstance(d, datetime.datetime))
1045 for d in (start, end)
1046 ):
1047 format = lambda dt: format_date(dt, locale=locale)
1048 elif all(
1049 # Both are times
1050 (isinstance(d, datetime.time) and not isinstance(d, datetime.date))
1051 for d in (start, end)
1052 ):
1053 format = lambda dt: format_time(dt, tzinfo=tzinfo, locale=locale)
1054 else:
1055 format = lambda dt: format_datetime(dt, tzinfo=tzinfo, locale=locale)
1057 formatted_start = format(start)
1058 formatted_end = format(end)
1060 if formatted_start == formatted_end:
1061 return format(start)
1063 return (
1064 locale.interval_formats.get(None, "{0}-{1}")
1065 .replace("{0}", formatted_start)
1066 .replace("{1}", formatted_end)
1067 )
1070def format_interval(
1071 start: _Instant,
1072 end: _Instant,
1073 skeleton: str | None = None,
1074 tzinfo: datetime.tzinfo | None = None,
1075 fuzzy: bool = True,
1076 locale: Locale | str | None = None,
1077) -> str:
1078 """
1079 Format an interval between two instants according to the locale's rules.
1081 >>> from datetime import date, time
1082 >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "yMd", locale="fi")
1083 '15.–17.1.2016'
1085 >>> format_interval(time(12, 12), time(16, 16), "Hm", locale="en_GB")
1086 '12:12–16:16'
1088 >>> format_interval(time(5, 12), time(16, 16), "hm", locale="en_US")
1089 '5:12\\u202fAM\\u2009–\\u20094:16\\u202fPM'
1091 >>> format_interval(time(16, 18), time(16, 24), "Hm", locale="it")
1092 '16:18–16:24'
1094 If the start instant equals the end instant, the interval is formatted like the instant.
1096 >>> format_interval(time(16, 18), time(16, 18), "Hm", locale="it")
1097 '16:18'
1099 Unknown skeletons fall back to "default" formatting.
1101 >>> format_interval(date(2015, 1, 1), date(2017, 1, 1), "wzq", locale="ja")
1102 '2015/01/01~2017/01/01'
1104 >>> format_interval(time(16, 18), time(16, 24), "xxx", locale="ja")
1105 '16:18:00~16:24:00'
1107 >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "xxx", locale="de")
1108 '15.01.2016\\u2009–\\u200917.01.2016'
1110 :param start: First instant (datetime/date/time)
1111 :param end: Second instant (datetime/date/time)
1112 :param skeleton: The "skeleton format" to use for formatting.
1113 :param tzinfo: tzinfo to use (if none is already attached)
1114 :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's
1115 close enough to it.
1116 :param locale: A locale object or identifier. Defaults to the system time locale.
1117 :return: Formatted interval
1118 """
1119 locale = Locale.parse(locale or LC_TIME)
1121 # NB: The quote comments below are from the algorithm description in
1122 # https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats
1124 # > Look for the intervalFormatItem element that matches the "skeleton",
1125 # > starting in the current locale and then following the locale fallback
1126 # > chain up to, but not including root.
1128 interval_formats = locale.interval_formats
1130 if skeleton not in interval_formats or not skeleton:
1131 # > If no match was found from the previous step, check what the closest
1132 # > match is in the fallback locale chain, as in availableFormats. That
1133 # > is, this allows for adjusting the string value field's width,
1134 # > including adjusting between "MMM" and "MMMM", and using different
1135 # > variants of the same field, such as 'v' and 'z'.
1136 if skeleton and fuzzy:
1137 skeleton = match_skeleton(skeleton, interval_formats)
1138 else:
1139 skeleton = None
1140 if not skeleton: # Still no match whatsoever?
1141 # > Otherwise, format the start and end datetime using the fallback pattern.
1142 return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
1144 skel_formats = interval_formats[skeleton]
1146 if start == end:
1147 return format_skeleton(skeleton, start, tzinfo, fuzzy=fuzzy, locale=locale)
1149 start = _ensure_datetime_tzinfo(_get_datetime(start), tzinfo=tzinfo)
1150 end = _ensure_datetime_tzinfo(_get_datetime(end), tzinfo=tzinfo)
1152 start_fmt = DateTimeFormat(start, locale=locale)
1153 end_fmt = DateTimeFormat(end, locale=locale)
1155 # > If a match is found from previous steps, compute the calendar field
1156 # > with the greatest difference between start and end datetime. If there
1157 # > is no difference among any of the fields in the pattern, format as a
1158 # > single date using availableFormats, and return.
1160 for field in PATTERN_CHAR_ORDER: # These are in largest-to-smallest order
1161 if field in skel_formats and start_fmt.extract(field) != end_fmt.extract(field):
1162 # > If there is a match, use the pieces of the corresponding pattern to
1163 # > format the start and end datetime, as above.
1164 return "".join(
1165 parse_pattern(pattern).apply(instant, locale)
1166 for pattern, instant in zip(skel_formats[field], (start, end))
1167 )
1169 # > Otherwise, format the start and end datetime using the fallback pattern.
1171 return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
1174def get_period_id(
1175 time: _Instant,
1176 tzinfo: datetime.tzinfo | None = None,
1177 type: Literal['selection'] | None = None,
1178 locale: Locale | str | None = None,
1179) -> str:
1180 """
1181 Get the day period ID for a given time.
1183 This ID can be used as a key for the period name dictionary.
1185 >>> from datetime import time
1186 >>> get_period_names(locale="de")[get_period_id(time(7, 42), locale="de")]
1187 'Morgen'
1189 >>> get_period_id(time(0), locale="en_US")
1190 'midnight'
1192 >>> get_period_id(time(0), type="selection", locale="en_US")
1193 'morning1'
1195 :param time: The time to inspect.
1196 :param tzinfo: The timezone for the time. See ``format_time``.
1197 :param type: The period type to use. Either "selection" or None.
1198 The selection type is used for selecting among phrases such as
1199 “Your email arrived yesterday evening” or “Your email arrived last night”.
1200 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
1201 :return: period ID. Something is always returned -- even if it's just "am" or "pm".
1202 """
1203 time = _get_time(time, tzinfo)
1204 seconds_past_midnight = int(time.hour * 60 * 60 + time.minute * 60 + time.second)
1205 locale = Locale.parse(locale or LC_TIME)
1207 # The LDML rules state that the rules may not overlap, so iterating in arbitrary
1208 # order should be alright, though `at` periods should be preferred.
1209 rulesets = locale.day_period_rules.get(type, {}).items()
1211 for rule_id, rules in rulesets:
1212 for rule in rules:
1213 if "at" in rule and rule["at"] == seconds_past_midnight:
1214 return rule_id
1216 for rule_id, rules in rulesets:
1217 for rule in rules:
1218 if "from" in rule and "before" in rule:
1219 if rule["from"] < rule["before"]:
1220 if rule["from"] <= seconds_past_midnight < rule["before"]:
1221 return rule_id
1222 else:
1223 # e.g. from="21:00" before="06:00"
1224 if (
1225 rule["from"] <= seconds_past_midnight < 86400
1226 or 0 <= seconds_past_midnight < rule["before"]
1227 ):
1228 return rule_id
1230 start_ok = end_ok = False
1232 if "from" in rule and seconds_past_midnight >= rule["from"]:
1233 start_ok = True
1234 if "to" in rule and seconds_past_midnight <= rule["to"]:
1235 # This rule type does not exist in the present CLDR data;
1236 # excuse the lack of test coverage.
1237 end_ok = True
1238 if "before" in rule and seconds_past_midnight < rule["before"]:
1239 end_ok = True
1240 if "after" in rule:
1241 raise NotImplementedError("'after' is deprecated as of CLDR 29.")
1243 if start_ok and end_ok:
1244 return rule_id
1246 if seconds_past_midnight < 43200:
1247 return "am"
1248 else:
1249 return "pm"
1252class ParseError(ValueError):
1253 pass
1256def parse_date(
1257 string: str,
1258 locale: Locale | str | None = None,
1259 format: _PredefinedTimeFormat | str = 'medium',
1260) -> datetime.date:
1261 """Parse a date from a string.
1263 If an explicit format is provided, it is used to parse the date.
1265 >>> parse_date('01.04.2004', format='dd.MM.yyyy')
1266 datetime.date(2004, 4, 1)
1268 If no format is given, or if it is one of "full", "long", "medium",
1269 or "short", the function first tries to interpret the string as
1270 ISO-8601 date format and then uses the date format for the locale
1271 as a hint to determine the order in which the date fields appear in
1272 the string.
1274 >>> parse_date('4/1/04', locale='en_US')
1275 datetime.date(2004, 4, 1)
1276 >>> parse_date('01.04.2004', locale='de_DE')
1277 datetime.date(2004, 4, 1)
1278 >>> parse_date('2004-04-01', locale='en_US')
1279 datetime.date(2004, 4, 1)
1280 >>> parse_date('2004-04-01', locale='de_DE')
1281 datetime.date(2004, 4, 1)
1282 >>> parse_date('01.04.04', locale='de_DE', format='short')
1283 datetime.date(2004, 4, 1)
1285 :param string: the string containing the date
1286 :param locale: a `Locale` object or a locale identifier
1287 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
1288 :param format: the format to use, either an explicit date format,
1289 or one of "full", "long", "medium", or "short"
1290 (see ``get_time_format``)
1291 """
1292 numbers = re.findall(r'(\d+)', string)
1293 if not numbers:
1294 raise ParseError("No numbers were found in input")
1296 use_predefined_format = format in ('full', 'long', 'medium', 'short')
1297 # we try ISO-8601 format first, meaning similar to formats
1298 # extended YYYY-MM-DD or basic YYYYMMDD
1299 iso_alike = re.match(
1300 r'^(\d{4})-?([01]\d)-?([0-3]\d)$',
1301 string,
1302 flags=re.ASCII, # allow only ASCII digits
1303 )
1304 if iso_alike and use_predefined_format:
1305 try:
1306 return datetime.date(*map(int, iso_alike.groups()))
1307 except ValueError:
1308 pass # a locale format might fit better, so let's continue
1310 if use_predefined_format:
1311 fmt = get_date_format(format=format, locale=locale)
1312 else:
1313 fmt = parse_pattern(format)
1314 format_str = fmt.pattern.lower()
1315 year_idx = format_str.index('y')
1316 month_idx = format_str.find('m')
1317 if month_idx < 0:
1318 month_idx = format_str.index('l')
1319 day_idx = format_str.index('d')
1321 indexes = sorted([(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')])
1322 indexes = {item[1]: idx for idx, item in enumerate(indexes)}
1324 # FIXME: this currently only supports numbers, but should also support month
1325 # names, both in the requested locale, and english
1327 year = numbers[indexes['Y']]
1328 year = 2000 + int(year) if len(year) == 2 else int(year)
1329 month = int(numbers[indexes['M']])
1330 day = int(numbers[indexes['D']])
1331 if month > 12:
1332 month, day = day, month
1333 return datetime.date(year, month, day)
1336def parse_time(
1337 string: str,
1338 locale: Locale | str | None = None,
1339 format: _PredefinedTimeFormat | str = 'medium',
1340) -> datetime.time:
1341 """Parse a time from a string.
1343 This function uses the time format for the locale as a hint to determine
1344 the order in which the time fields appear in the string.
1346 If an explicit format is provided, the function will use it to parse
1347 the time instead.
1349 >>> parse_time('15:30:00', locale='en_US')
1350 datetime.time(15, 30)
1351 >>> parse_time('15:30:00', format='H:mm:ss')
1352 datetime.time(15, 30)
1354 :param string: the string containing the time
1355 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
1356 :param format: the format to use, either an explicit time format,
1357 or one of "full", "long", "medium", or "short"
1358 (see ``get_time_format``)
1359 :return: the parsed time
1360 :rtype: `time`
1361 """
1362 numbers = re.findall(r'(\d+)', string)
1363 if not numbers:
1364 raise ParseError("No numbers were found in input")
1366 # TODO: try ISO format first?
1367 if format in ('full', 'long', 'medium', 'short'):
1368 fmt = get_time_format(format=format, locale=locale)
1369 else:
1370 fmt = parse_pattern(format)
1371 format_str = fmt.pattern.lower()
1372 hour_idx = format_str.find('h')
1373 if hour_idx < 0:
1374 hour_idx = format_str.index('k')
1375 min_idx = format_str.index('m')
1376 # format might not contain seconds
1377 if (sec_idx := format_str.find('s')) < 0:
1378 sec_idx = math.inf
1380 indexes = sorted([(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')])
1381 indexes = {item[1]: idx for idx, item in enumerate(indexes)}
1383 # TODO: support time zones
1385 # Check if the format specifies a period to be used;
1386 # if it does, look for 'pm' to figure out an offset.
1387 hour_offset = 0
1388 if 'a' in format_str and 'pm' in string.lower():
1389 hour_offset = 12
1391 # Parse up to three numbers from the string.
1392 minute = second = 0
1393 hour = int(numbers[indexes['H']]) + hour_offset
1394 if len(numbers) > 1:
1395 minute = int(numbers[indexes['M']])
1396 if len(numbers) > 2:
1397 second = int(numbers[indexes['S']])
1398 return datetime.time(hour, minute, second)
1401class DateTimePattern:
1402 def __init__(self, pattern: str, format: DateTimeFormat):
1403 self.pattern = pattern
1404 self.format = format
1406 def __repr__(self) -> str:
1407 return f"<{type(self).__name__} {self.pattern!r}>"
1409 def __str__(self) -> str:
1410 pat = self.pattern
1411 return pat
1413 def __mod__(self, other: DateTimeFormat) -> str:
1414 if not isinstance(other, DateTimeFormat):
1415 return NotImplemented
1416 return self.format % other
1418 def apply(
1419 self,
1420 datetime: datetime.date | datetime.time,
1421 locale: Locale | str | None,
1422 reference_date: datetime.date | None = None,
1423 ) -> str:
1424 return self % DateTimeFormat(datetime, locale, reference_date)
1427class DateTimeFormat:
1428 def __init__(
1429 self,
1430 value: datetime.date | datetime.time,
1431 locale: Locale | str,
1432 reference_date: datetime.date | None = None,
1433 ) -> None:
1434 assert isinstance(value, (datetime.date, datetime.datetime, datetime.time))
1435 if isinstance(value, (datetime.datetime, datetime.time)) and value.tzinfo is None:
1436 value = value.replace(tzinfo=UTC)
1437 self.value = value
1438 self.locale = Locale.parse(locale)
1439 self.reference_date = reference_date
1441 def __getitem__(self, name: str) -> str:
1442 char = name[0]
1443 num = len(name)
1444 if char == 'G':
1445 return self.format_era(char, num)
1446 elif char in ('y', 'Y', 'u'):
1447 return self.format_year(char, num)
1448 elif char in ('Q', 'q'):
1449 return self.format_quarter(char, num)
1450 elif char in ('M', 'L'):
1451 return self.format_month(char, num)
1452 elif char in ('w', 'W'):
1453 return self.format_week(char, num)
1454 elif char == 'd':
1455 return self.format(self.value.day, num)
1456 elif char == 'D':
1457 return self.format_day_of_year(num)
1458 elif char == 'F':
1459 return self.format_day_of_week_in_month()
1460 elif char in ('E', 'e', 'c'):
1461 return self.format_weekday(char, num)
1462 elif char in ('a', 'b', 'B'):
1463 return self.format_period(char, num)
1464 elif char == 'h':
1465 if self.value.hour % 12 == 0:
1466 return self.format(12, num)
1467 else:
1468 return self.format(self.value.hour % 12, num)
1469 elif char == 'H':
1470 return self.format(self.value.hour, num)
1471 elif char == 'K':
1472 return self.format(self.value.hour % 12, num)
1473 elif char == 'k':
1474 if self.value.hour == 0:
1475 return self.format(24, num)
1476 else:
1477 return self.format(self.value.hour, num)
1478 elif char == 'm':
1479 return self.format(self.value.minute, num)
1480 elif char == 's':
1481 return self.format(self.value.second, num)
1482 elif char == 'S':
1483 return self.format_frac_seconds(num)
1484 elif char == 'A':
1485 return self.format_milliseconds_in_day(num)
1486 elif char in ('z', 'Z', 'v', 'V', 'x', 'X', 'O'):
1487 return self.format_timezone(char, num)
1488 else:
1489 raise KeyError(f"Unsupported date/time field {char!r}")
1491 def extract(self, char: str) -> int:
1492 char = str(char)[0]
1493 if char == 'y':
1494 return self.value.year
1495 elif char == 'M':
1496 return self.value.month
1497 elif char == 'd':
1498 return self.value.day
1499 elif char == 'H':
1500 return self.value.hour
1501 elif char == 'h':
1502 return self.value.hour % 12 or 12
1503 elif char == 'm':
1504 return self.value.minute
1505 elif char == 'a':
1506 return int(self.value.hour >= 12) # 0 for am, 1 for pm
1507 else:
1508 raise NotImplementedError(
1509 f"Not implemented: extracting {char!r} from {self.value!r}",
1510 )
1512 def format_era(self, char: str, num: int) -> str:
1513 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)]
1514 era = int(self.value.year >= 0)
1515 return get_era_names(width, self.locale)[era]
1517 def format_year(self, char: str, num: int) -> str:
1518 value = self.value.year
1519 if char.isupper():
1520 month = self.value.month
1521 if month == 1 and self.value.day < 7 and self.get_week_of_year() >= 52:
1522 value -= 1
1523 elif month == 12 and self.value.day > 25 and self.get_week_of_year() <= 2:
1524 value += 1
1525 year = self.format(value, num)
1526 if num == 2:
1527 year = year[-2:]
1528 return year
1530 def format_quarter(self, char: str, num: int) -> str:
1531 quarter = (self.value.month - 1) // 3 + 1
1532 if num <= 2:
1533 return '%0*d' % (num, quarter)
1534 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
1535 context = {'Q': 'format', 'q': 'stand-alone'}[char]
1536 return get_quarter_names(width, context, self.locale)[quarter]
1538 def format_month(self, char: str, num: int) -> str:
1539 if num <= 2:
1540 return '%0*d' % (num, self.value.month)
1541 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
1542 context = {'M': 'format', 'L': 'stand-alone'}[char]
1543 return get_month_names(width, context, self.locale)[self.value.month]
1545 def format_week(self, char: str, num: int) -> str:
1546 if char.islower(): # week of year
1547 week = self.get_week_of_year()
1548 return self.format(week, num)
1549 else: # week of month
1550 week = self.get_week_of_month()
1551 return str(week)
1553 def format_weekday(self, char: str = 'E', num: int = 4) -> str:
1554 """
1555 Return weekday from parsed datetime according to format pattern.
1557 >>> from datetime import date
1558 >>> format = DateTimeFormat(date(2016, 2, 28), Locale.parse('en_US'))
1559 >>> format.format_weekday()
1560 'Sunday'
1562 'E': Day of week - Use one through three letters for the abbreviated day name, four for the full (wide) name,
1563 five for the narrow name, or six for the short name.
1564 >>> format.format_weekday('E',2)
1565 'Sun'
1567 'e': Local day of week. Same as E except adds a numeric value that will depend on the local starting day of the
1568 week, using one or two letters. For this example, Monday is the first day of the week.
1569 >>> format.format_weekday('e',2)
1570 '01'
1572 'c': Stand-Alone local day of week - Use one letter for the local numeric value (same as 'e'), three for the
1573 abbreviated day name, four for the full (wide) name, five for the narrow name, or six for the short name.
1574 >>> format.format_weekday('c',1)
1575 '1'
1577 :param char: pattern format character ('e','E','c')
1578 :param num: count of format character
1580 """
1581 if num < 3:
1582 if char.islower():
1583 value = 7 - self.locale.first_week_day + self.value.weekday()
1584 return self.format(value % 7 + 1, num)
1585 num = 3
1586 weekday = self.value.weekday()
1587 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow', 6: 'short'}[num]
1588 context = "stand-alone" if char == "c" else "format"
1589 return get_day_names(width, context, self.locale)[weekday]
1591 def format_day_of_year(self, num: int) -> str:
1592 return self.format(self.get_day_of_year(), num)
1594 def format_day_of_week_in_month(self) -> str:
1595 return str((self.value.day - 1) // 7 + 1)
1597 def format_period(self, char: str, num: int) -> str:
1598 """
1599 Return period from parsed datetime according to format pattern.
1601 >>> from datetime import datetime, time
1602 >>> format = DateTimeFormat(time(13, 42), 'fi_FI')
1603 >>> format.format_period('a', 1)
1604 'ip.'
1605 >>> format.format_period('b', 1)
1606 'iltap.'
1607 >>> format.format_period('b', 4)
1608 'iltapäivä'
1609 >>> format.format_period('B', 4)
1610 'iltapäivällä'
1611 >>> format.format_period('B', 5)
1612 'ip.'
1614 >>> format = DateTimeFormat(datetime(2022, 4, 28, 6, 27), 'zh_Hant')
1615 >>> format.format_period('a', 1)
1616 '上午'
1617 >>> format.format_period('B', 1)
1618 '清晨'
1620 :param char: pattern format character ('a', 'b', 'B')
1621 :param num: count of format character
1623 """
1624 widths = [
1625 {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)],
1626 'wide',
1627 'narrow',
1628 'abbreviated',
1629 ]
1630 if char == 'a':
1631 period = 'pm' if self.value.hour >= 12 else 'am'
1632 context = 'format'
1633 else:
1634 period = get_period_id(self.value, locale=self.locale)
1635 context = 'format' if char == 'B' else 'stand-alone'
1636 for width in widths:
1637 period_names = get_period_names(context=context, width=width, locale=self.locale)
1638 if period in period_names:
1639 return period_names[period]
1640 raise ValueError(f"Could not format period {period} in {self.locale}")
1642 def format_frac_seconds(self, num: int) -> str:
1643 """ Return fractional seconds.
1645 Rounds the time's microseconds to the precision given by the number \
1646 of digits passed in.
1647 """
1648 value = self.value.microsecond / 1000000
1649 return self.format(round(value, num) * 10**num, num)
1651 def format_milliseconds_in_day(self, num):
1652 msecs = (
1653 self.value.microsecond // 1000
1654 + self.value.second * 1000
1655 + self.value.minute * 60000
1656 + self.value.hour * 3600000
1657 )
1658 return self.format(msecs, num)
1660 def format_timezone(self, char: str, num: int) -> str:
1661 width = {3: 'short', 4: 'long', 5: 'iso8601'}[max(3, num)]
1663 # It could be that we only receive a time to format, but also have a
1664 # reference date which is important to distinguish between timezone
1665 # variants (summer/standard time)
1666 value = self.value
1667 if self.reference_date:
1668 value = datetime.datetime.combine(self.reference_date, self.value)
1670 if char == 'z':
1671 return get_timezone_name(value, width, locale=self.locale)
1672 elif char == 'Z':
1673 if num == 5:
1674 return get_timezone_gmt(value, width, locale=self.locale, return_z=True)
1675 return get_timezone_gmt(value, width, locale=self.locale)
1676 elif char == 'O':
1677 if num == 4:
1678 return get_timezone_gmt(value, width, locale=self.locale)
1679 # TODO: To add support for O:1
1680 elif char == 'v':
1681 return get_timezone_name(value.tzinfo, width, locale=self.locale)
1682 elif char == 'V':
1683 if num == 1:
1684 return get_timezone_name(value.tzinfo, width, locale=self.locale)
1685 elif num == 2:
1686 return get_timezone_name(value.tzinfo, locale=self.locale, return_zone=True)
1687 elif num == 3:
1688 return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True) # fmt: skip
1689 return get_timezone_location(value.tzinfo, locale=self.locale)
1690 elif char in 'Xx':
1691 return_z = char == 'X'
1692 if num == 1:
1693 width = 'iso8601_short'
1694 elif num in (2, 4):
1695 width = 'short'
1696 elif num in (3, 5):
1697 width = 'iso8601'
1698 return get_timezone_gmt(value, width=width, locale=self.locale, return_z=return_z) # fmt: skip
1700 def format(self, value: SupportsInt, length: int) -> str:
1701 return '%0*d' % (length, value)
1703 def get_day_of_year(self, date: datetime.date | None = None) -> int:
1704 if date is None:
1705 date = self.value
1706 return (date - date.replace(month=1, day=1)).days + 1
1708 def get_week_of_year(self) -> int:
1709 """Return the week of the year."""
1710 day_of_year = self.get_day_of_year(self.value)
1711 week = self.get_week_number(day_of_year)
1712 if week == 0:
1713 date = datetime.date(self.value.year - 1, 12, 31)
1714 week = self.get_week_number(self.get_day_of_year(date), date.weekday())
1715 elif week > 52:
1716 weekday = datetime.date(self.value.year + 1, 1, 1).weekday()
1717 if (
1718 self.get_week_number(1, weekday) == 1
1719 and 32 - (weekday - self.locale.first_week_day) % 7 <= self.value.day
1720 ):
1721 week = 1
1722 return week
1724 def get_week_of_month(self) -> int:
1725 """Return the week of the month."""
1726 return self.get_week_number(self.value.day)
1728 def get_week_number(self, day_of_period: int, day_of_week: int | None = None) -> int:
1729 """Return the number of the week of a day within a period. This may be
1730 the week number in a year or the week number in a month.
1732 Usually this will return a value equal to or greater than 1, but if the
1733 first week of the period is so short that it actually counts as the last
1734 week of the previous period, this function will return 0.
1736 >>> date = datetime.date(2006, 1, 8)
1737 >>> DateTimeFormat(date, 'de_DE').get_week_number(6)
1738 1
1739 >>> DateTimeFormat(date, 'en_US').get_week_number(6)
1740 2
1742 :param day_of_period: the number of the day in the period (usually
1743 either the day of month or the day of year)
1744 :param day_of_week: the week day; if omitted, the week day of the
1745 current date is assumed
1746 """
1747 if day_of_week is None:
1748 day_of_week = self.value.weekday()
1749 first_day = (day_of_week - self.locale.first_week_day - day_of_period + 1) % 7
1750 if first_day < 0:
1751 first_day += 7
1752 week_number = (day_of_period + first_day - 1) // 7
1753 if 7 - first_day >= self.locale.min_week_days:
1754 week_number += 1
1755 return week_number
1758PATTERN_CHARS: dict[str, list[int] | None] = {
1759 'G': [1, 2, 3, 4, 5], # era
1760 'y': None, 'Y': None, 'u': None, # year
1761 'Q': [1, 2, 3, 4, 5], 'q': [1, 2, 3, 4, 5], # quarter
1762 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month
1763 'w': [1, 2], 'W': [1], # week
1764 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day
1765 'E': [1, 2, 3, 4, 5, 6], 'e': [1, 2, 3, 4, 5, 6], 'c': [1, 3, 4, 5, 6], # week day
1766 'a': [1, 2, 3, 4, 5], 'b': [1, 2, 3, 4, 5], 'B': [1, 2, 3, 4, 5], # period
1767 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour
1768 'm': [1, 2], # minute
1769 's': [1, 2], 'S': None, 'A': None, # second
1770 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4, 5], 'O': [1, 4], 'v': [1, 4], # zone
1771 'V': [1, 2, 3, 4], 'x': [1, 2, 3, 4, 5], 'X': [1, 2, 3, 4, 5], # zone
1772} # fmt: skip
1774#: The pattern characters declared in the Date Field Symbol Table
1775#: (https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
1776#: in order of decreasing magnitude.
1777PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx"
1780def parse_pattern(pattern: str | DateTimePattern) -> DateTimePattern:
1781 """Parse date, time, and datetime format patterns.
1783 >>> parse_pattern("MMMMd").format
1784 '%(MMMM)s%(d)s'
1785 >>> parse_pattern("MMM d, yyyy").format
1786 '%(MMM)s %(d)s, %(yyyy)s'
1788 Pattern can contain literal strings in single quotes:
1790 >>> parse_pattern("H:mm' Uhr 'z").format
1791 '%(H)s:%(mm)s Uhr %(z)s'
1793 An actual single quote can be used by using two adjacent single quote
1794 characters:
1796 >>> parse_pattern("hh' o''clock'").format
1797 "%(hh)s o'clock"
1799 :param pattern: the formatting pattern to parse
1800 """
1801 if isinstance(pattern, DateTimePattern):
1802 return pattern
1803 return _cached_parse_pattern(pattern)
1806@lru_cache(maxsize=1024)
1807def _cached_parse_pattern(pattern: str) -> DateTimePattern:
1808 result = []
1810 for tok_type, tok_value in tokenize_pattern(pattern):
1811 if tok_type == "chars":
1812 result.append(tok_value.replace('%', '%%'))
1813 elif tok_type == "field":
1814 fieldchar, fieldnum = tok_value
1815 limit = PATTERN_CHARS[fieldchar]
1816 if limit and fieldnum not in limit:
1817 raise ValueError(f"Invalid length for field: {fieldchar * fieldnum!r}")
1818 result.append('%%(%s)s' % (fieldchar * fieldnum))
1819 else:
1820 raise NotImplementedError(f"Unknown token type: {tok_type}")
1821 return DateTimePattern(pattern, ''.join(result))
1824def tokenize_pattern(pattern: str) -> list[tuple[str, str | tuple[str, int]]]:
1825 """
1826 Tokenize date format patterns.
1828 Returns a list of (token_type, token_value) tuples.
1830 ``token_type`` may be either "chars" or "field".
1832 For "chars" tokens, the value is the literal value.
1834 For "field" tokens, the value is a tuple of (field character, repetition count).
1836 :param pattern: Pattern string
1837 :type pattern: str
1838 :rtype: list[tuple]
1839 """
1840 result = []
1841 quotebuf = None
1842 charbuf = []
1843 fieldchar = ['']
1844 fieldnum = [0]
1846 def append_chars():
1847 result.append(('chars', ''.join(charbuf).replace('\0', "'")))
1848 del charbuf[:]
1850 def append_field():
1851 result.append(('field', (fieldchar[0], fieldnum[0])))
1852 fieldchar[0] = ''
1853 fieldnum[0] = 0
1855 for char in pattern.replace("''", '\0'):
1856 if quotebuf is None:
1857 if char == "'": # quote started
1858 if fieldchar[0]:
1859 append_field()
1860 elif charbuf:
1861 append_chars()
1862 quotebuf = []
1863 elif char in PATTERN_CHARS:
1864 if charbuf:
1865 append_chars()
1866 if char == fieldchar[0]:
1867 fieldnum[0] += 1
1868 else:
1869 if fieldchar[0]:
1870 append_field()
1871 fieldchar[0] = char
1872 fieldnum[0] = 1
1873 else:
1874 if fieldchar[0]:
1875 append_field()
1876 charbuf.append(char)
1878 elif quotebuf is not None:
1879 if char == "'": # end of quote
1880 charbuf.extend(quotebuf)
1881 quotebuf = None
1882 else: # inside quote
1883 quotebuf.append(char)
1885 if fieldchar[0]:
1886 append_field()
1887 elif charbuf:
1888 append_chars()
1890 return result
1893def untokenize_pattern(tokens: Iterable[tuple[str, str | tuple[str, int]]]) -> str:
1894 """
1895 Turn a date format pattern token stream back into a string.
1897 This is the reverse operation of ``tokenize_pattern``.
1899 :type tokens: Iterable[tuple]
1900 :rtype: str
1901 """
1902 output = []
1903 for tok_type, tok_value in tokens:
1904 if tok_type == "field":
1905 output.append(tok_value[0] * tok_value[1])
1906 elif tok_type == "chars":
1907 if not any(ch in PATTERN_CHARS for ch in tok_value): # No need to quote
1908 output.append(tok_value)
1909 else:
1910 output.append("'%s'" % tok_value.replace("'", "''"))
1911 return "".join(output)
1914def split_interval_pattern(pattern: str) -> list[str]:
1915 """
1916 Split an interval-describing datetime pattern into multiple pieces.
1918 > The pattern is then designed to be broken up into two pieces by determining the first repeating field.
1919 - https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats
1921 >>> split_interval_pattern('E d.M. – E d.M.')
1922 ['E d.M. – ', 'E d.M.']
1923 >>> split_interval_pattern("Y 'text' Y 'more text'")
1924 ["Y 'text '", "Y 'more text'"]
1925 >>> split_interval_pattern('E, MMM d – E')
1926 ['E, MMM d – ', 'E']
1927 >>> split_interval_pattern("MMM d")
1928 ['MMM d']
1929 >>> split_interval_pattern("y G")
1930 ['y G']
1931 >>> split_interval_pattern('MMM d – d')
1932 ['MMM d – ', 'd']
1934 :param pattern: Interval pattern string
1935 :return: list of "subpatterns"
1936 """
1938 seen_fields = set()
1939 parts = [[]]
1941 for tok_type, tok_value in tokenize_pattern(pattern):
1942 if tok_type == "field":
1943 if tok_value[0] in seen_fields: # Repeated field
1944 parts.append([])
1945 seen_fields.clear()
1946 seen_fields.add(tok_value[0])
1947 parts[-1].append((tok_type, tok_value))
1949 return [untokenize_pattern(tokens) for tokens in parts]
1952def match_skeleton(
1953 skeleton: str,
1954 options: Iterable[str],
1955 allow_different_fields: bool = False,
1956) -> str | None:
1957 """
1958 Find the closest match for the given datetime skeleton among the options given.
1960 This uses the rules outlined in the TR35 document.
1962 >>> match_skeleton('yMMd', ('yMd', 'yMMMd'))
1963 'yMd'
1965 >>> match_skeleton('yMMd', ('jyMMd',), allow_different_fields=True)
1966 'jyMMd'
1968 >>> match_skeleton('yMMd', ('qyMMd',), allow_different_fields=False)
1970 >>> match_skeleton('hmz', ('hmv',))
1971 'hmv'
1973 :param skeleton: The skeleton to match
1974 :type skeleton: str
1975 :param options: An iterable of other skeletons to match against
1976 :type options: Iterable[str]
1977 :param allow_different_fields: Whether to allow a match that uses different fields
1978 than the skeleton requested.
1979 :type allow_different_fields: bool
1981 :return: The closest skeleton match, or if no match was found, None.
1982 :rtype: str|None
1983 """
1985 # TODO: maybe implement pattern expansion?
1987 # Based on the implementation in
1988 # https://github.com/unicode-org/icu/blob/main/icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalInfo.java
1990 # Filter out falsy values and sort for stability; when `interval_formats` is passed in, there may be a None key.
1991 options = sorted(option for option in options if option)
1993 if 'z' in skeleton and not any('z' in option for option in options):
1994 skeleton = skeleton.replace('z', 'v')
1995 if 'k' in skeleton and not any('k' in option for option in options):
1996 skeleton = skeleton.replace('k', 'H')
1997 if 'K' in skeleton and not any('K' in option for option in options):
1998 skeleton = skeleton.replace('K', 'h')
1999 if 'a' in skeleton and not any('a' in option for option in options):
2000 skeleton = skeleton.replace('a', '')
2001 if 'b' in skeleton and not any('b' in option for option in options):
2002 skeleton = skeleton.replace('b', '')
2004 get_input_field_width = dict(t[1] for t in tokenize_pattern(skeleton) if t[0] == "field").get # fmt: skip
2005 best_skeleton = None
2006 best_distance = None
2007 for option in options:
2008 get_opt_field_width = dict(t[1] for t in tokenize_pattern(option) if t[0] == "field").get # fmt: skip
2009 distance = 0
2010 for field in PATTERN_CHARS:
2011 input_width = get_input_field_width(field, 0)
2012 opt_width = get_opt_field_width(field, 0)
2013 if input_width == opt_width:
2014 continue
2015 if opt_width == 0 or input_width == 0:
2016 if not allow_different_fields: # This one is not okay
2017 option = None
2018 break
2019 # Magic weight constant for "entirely different fields"
2020 distance += 0x1000
2021 elif field == 'M' and (
2022 (input_width > 2 and opt_width <= 2) or (input_width <= 2 and opt_width > 2)
2023 ):
2024 # Magic weight constant for "text turns into a number"
2025 distance += 0x100
2026 else:
2027 distance += abs(input_width - opt_width)
2029 if not option:
2030 # We lost the option along the way (probably due to "allow_different_fields")
2031 continue
2033 if not best_skeleton or distance < best_distance:
2034 best_skeleton = option
2035 best_distance = distance
2037 if distance == 0: # Found a perfect match!
2038 break
2040 return best_skeleton