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

731 statements  

1""" 

2 babel.dates 

3 ~~~~~~~~~~~ 

4 

5 Locale dependent formatting and parsing of dates and times. 

6 

7 The default locale for the functions in this module is determined by the 

8 following environment variables, in that order: 

9 

10 * ``LC_TIME``, 

11 * ``LC_ALL``, and 

12 * ``LANG`` 

13 

14 :copyright: (c) 2013-2025 by the Babel Team. 

15 :license: BSD, see LICENSE for more details. 

16""" 

17 

18from __future__ import annotations 

19 

20import math 

21import re 

22import warnings 

23from functools import lru_cache 

24from typing import TYPE_CHECKING, Literal, SupportsInt 

25 

26try: 

27 import pytz 

28except ModuleNotFoundError: 

29 pytz = None 

30 import zoneinfo 

31 

32import datetime 

33from collections.abc import Iterable 

34 

35from babel import localtime 

36from babel.core import Locale, default_locale, get_global 

37from babel.localedata import LocaleDataDict 

38 

39if TYPE_CHECKING: 

40 from typing_extensions import TypeAlias 

41 _Instant: TypeAlias = datetime.date | datetime.time | float | None 

42 _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short'] 

43 _Context: TypeAlias = Literal['format', 'stand-alone'] 

44 _DtOrTzinfo: TypeAlias = datetime.datetime | datetime.tzinfo | str | int | datetime.time | None 

45 

46# "If a given short metazone form is known NOT to be understood in a given 

47# locale and the parent locale has this value such that it would normally 

48# be inherited, the inheritance of this value can be explicitly disabled by 

49# use of the 'no inheritance marker' as the value, which is 3 simultaneous [sic] 

50# empty set characters ( U+2205 )." 

51# - https://www.unicode.org/reports/tr35/tr35-dates.html#Metazone_Names 

52 

53NO_INHERITANCE_MARKER = '\u2205\u2205\u2205' 

54 

55UTC = datetime.timezone.utc 

56LOCALTZ = localtime.LOCALTZ 

57 

58LC_TIME = default_locale('LC_TIME') 

59 

60 

61def _localize(tz: datetime.tzinfo, dt: datetime.datetime) -> datetime.datetime: 

62 # Support localizing with both pytz and zoneinfo tzinfos 

63 # nothing to do 

64 if dt.tzinfo is tz: 

65 return dt 

66 

67 if hasattr(tz, 'localize'): # pytz 

68 return tz.localize(dt) 

69 

70 if dt.tzinfo is None: 

71 # convert naive to localized 

72 return dt.replace(tzinfo=tz) 

73 

74 # convert timezones 

75 return dt.astimezone(tz) 

76 

77 

78def _get_dt_and_tzinfo(dt_or_tzinfo: _DtOrTzinfo) -> tuple[datetime.datetime | None, datetime.tzinfo]: 

79 """ 

80 Parse a `dt_or_tzinfo` value into a datetime and a tzinfo. 

81 

82 See the docs for this function's callers for semantics. 

83 

84 :rtype: tuple[datetime, tzinfo] 

85 """ 

86 if dt_or_tzinfo is None: 

87 dt = datetime.datetime.now() 

88 tzinfo = LOCALTZ 

89 elif isinstance(dt_or_tzinfo, str): 

90 dt = None 

91 tzinfo = get_timezone(dt_or_tzinfo) 

92 elif isinstance(dt_or_tzinfo, int): 

93 dt = None 

94 tzinfo = UTC 

95 elif isinstance(dt_or_tzinfo, (datetime.datetime, datetime.time)): 

96 dt = _get_datetime(dt_or_tzinfo) 

97 tzinfo = dt.tzinfo if dt.tzinfo is not None else UTC 

98 else: 

99 dt = None 

100 tzinfo = dt_or_tzinfo 

101 return dt, tzinfo 

102 

103 

104def _get_tz_name(dt_or_tzinfo: _DtOrTzinfo) -> str: 

105 """ 

106 Get the timezone name out of a time, datetime, or tzinfo object. 

107 

108 :rtype: str 

109 """ 

110 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) 

111 if hasattr(tzinfo, 'zone'): # pytz object 

112 return tzinfo.zone 

113 elif hasattr(tzinfo, 'key') and tzinfo.key is not None: # ZoneInfo object 

114 return tzinfo.key 

115 else: 

116 return tzinfo.tzname(dt or datetime.datetime.now(UTC)) 

117 

118 

119def _get_datetime(instant: _Instant) -> datetime.datetime: 

120 """ 

121 Get a datetime out of an "instant" (date, time, datetime, number). 

122 

123 .. warning:: The return values of this function may depend on the system clock. 

124 

125 If the instant is None, the current moment is used. 

126 If the instant is a time, it's augmented with today's date. 

127 

128 Dates are converted to naive datetimes with midnight as the time component. 

129 

130 >>> from datetime import date, datetime 

131 >>> _get_datetime(date(2015, 1, 1)) 

132 datetime.datetime(2015, 1, 1, 0, 0) 

133 

134 UNIX timestamps are converted to datetimes. 

135 

136 >>> _get_datetime(1400000000) 

137 datetime.datetime(2014, 5, 13, 16, 53, 20) 

138 

139 Other values are passed through as-is. 

140 

141 >>> x = datetime(2015, 1, 1) 

142 >>> _get_datetime(x) is x 

143 True 

144 

145 :param instant: date, time, datetime, integer, float or None 

146 :type instant: date|time|datetime|int|float|None 

147 :return: a datetime 

148 :rtype: datetime 

149 """ 

150 if instant is None: 

151 return datetime.datetime.now(UTC).replace(tzinfo=None) 

152 elif isinstance(instant, (int, float)): 

153 return datetime.datetime.fromtimestamp(instant, UTC).replace(tzinfo=None) 

154 elif isinstance(instant, datetime.time): 

155 return datetime.datetime.combine(datetime.date.today(), instant) 

156 elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): 

157 return datetime.datetime.combine(instant, datetime.time()) 

158 # TODO (3.x): Add an assertion/type check for this fallthrough branch: 

159 return instant 

160 

161 

162def _ensure_datetime_tzinfo(dt: datetime.datetime, tzinfo: datetime.tzinfo | None = None) -> datetime.datetime: 

163 """ 

164 Ensure the datetime passed has an attached tzinfo. 

165 

166 If the datetime is tz-naive to begin with, UTC is attached. 

167 

168 If a tzinfo is passed in, the datetime is normalized to that timezone. 

169 

170 >>> from datetime import datetime 

171 >>> _get_tz_name(_ensure_datetime_tzinfo(datetime(2015, 1, 1))) 

172 'UTC' 

173 

174 >>> tz = get_timezone("Europe/Stockholm") 

175 >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1, 13, 15, tzinfo=UTC), tzinfo=tz).hour 

176 14 

177 

178 :param datetime: Datetime to augment. 

179 :param tzinfo: optional tzinfo 

180 :return: datetime with tzinfo 

181 :rtype: datetime 

182 """ 

183 if dt.tzinfo is None: 

184 dt = dt.replace(tzinfo=UTC) 

185 if tzinfo is not None: 

186 dt = dt.astimezone(get_timezone(tzinfo)) 

187 if hasattr(tzinfo, 'normalize'): # pytz 

188 dt = tzinfo.normalize(dt) 

189 return dt 

190 

191 

192def _get_time( 

193 time: datetime.time | datetime.datetime | None, 

194 tzinfo: datetime.tzinfo | None = None, 

195) -> datetime.time: 

196 """ 

197 Get a timezoned time from a given instant. 

198 

199 .. warning:: The return values of this function may depend on the system clock. 

200 

201 :param time: time, datetime or None 

202 :rtype: time 

203 """ 

204 if time is None: 

205 time = datetime.datetime.now(UTC) 

206 elif isinstance(time, (int, float)): 

207 time = datetime.datetime.fromtimestamp(time, UTC) 

208 

209 if time.tzinfo is None: 

210 time = time.replace(tzinfo=UTC) 

211 

212 if isinstance(time, datetime.datetime): 

213 if tzinfo is not None: 

214 time = time.astimezone(tzinfo) 

215 if hasattr(tzinfo, 'normalize'): # pytz 

216 time = tzinfo.normalize(time) 

217 time = time.timetz() 

218 elif tzinfo is not None: 

219 time = time.replace(tzinfo=tzinfo) 

220 return time 

221 

222 

223def get_timezone(zone: str | datetime.tzinfo | None = None) -> datetime.tzinfo: 

224 """Looks up a timezone by name and returns it. The timezone object 

225 returned comes from ``pytz`` or ``zoneinfo``, whichever is available. 

226 It corresponds to the `tzinfo` interface and can be used with all of 

227 the functions of Babel that operate with dates. 

228 

229 If a timezone is not known a :exc:`LookupError` is raised. If `zone` 

230 is ``None`` a local zone object is returned. 

231 

232 :param zone: the name of the timezone to look up. If a timezone object 

233 itself is passed in, it's returned unchanged. 

234 """ 

235 if zone is None: 

236 return LOCALTZ 

237 if not isinstance(zone, str): 

238 return zone 

239 

240 if pytz: 

241 try: 

242 return pytz.timezone(zone) 

243 except pytz.UnknownTimeZoneError as e: 

244 exc = e 

245 else: 

246 assert zoneinfo 

247 try: 

248 return zoneinfo.ZoneInfo(zone) 

249 except zoneinfo.ZoneInfoNotFoundError as e: 

250 exc = e 

251 

252 raise LookupError(f"Unknown timezone {zone}") from exc 

253 

254 

255def get_period_names( 

256 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

257 context: _Context = 'stand-alone', 

258 locale: Locale | str | None = None, 

259) -> LocaleDataDict: 

260 """Return the names for day periods (AM/PM) used by the locale. 

261 

262 >>> get_period_names(locale='en_US')['am'] 

263 u'AM' 

264 

265 :param width: the width to use, one of "abbreviated", "narrow", or "wide" 

266 :param context: the context, either "format" or "stand-alone" 

267 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

268 """ 

269 return Locale.parse(locale or LC_TIME).day_periods[context][width] 

270 

271 

272def get_day_names( 

273 width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide', 

274 context: _Context = 'format', 

275 locale: Locale | str | None = None, 

276) -> LocaleDataDict: 

277 """Return the day names used by the locale for the specified format. 

278 

279 >>> get_day_names('wide', locale='en_US')[1] 

280 u'Tuesday' 

281 >>> get_day_names('short', locale='en_US')[1] 

282 u'Tu' 

283 >>> get_day_names('abbreviated', locale='es')[1] 

284 u'mar' 

285 >>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1] 

286 u'D' 

287 

288 :param width: the width to use, one of "wide", "abbreviated", "short" or "narrow" 

289 :param context: the context, either "format" or "stand-alone" 

290 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

291 """ 

292 return Locale.parse(locale or LC_TIME).days[context][width] 

293 

294 

295def get_month_names( 

296 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

297 context: _Context = 'format', 

298 locale: Locale | str | None = None, 

299) -> LocaleDataDict: 

300 """Return the month names used by the locale for the specified format. 

301 

302 >>> get_month_names('wide', locale='en_US')[1] 

303 u'January' 

304 >>> get_month_names('abbreviated', locale='es')[1] 

305 u'ene' 

306 >>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1] 

307 u'J' 

308 

309 :param width: the width to use, one of "wide", "abbreviated", or "narrow" 

310 :param context: the context, either "format" or "stand-alone" 

311 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

312 """ 

313 return Locale.parse(locale or LC_TIME).months[context][width] 

314 

315 

316def get_quarter_names( 

317 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

318 context: _Context = 'format', 

319 locale: Locale | str | None = None, 

320) -> LocaleDataDict: 

321 """Return the quarter names used by the locale for the specified format. 

322 

323 >>> get_quarter_names('wide', locale='en_US')[1] 

324 u'1st quarter' 

325 >>> get_quarter_names('abbreviated', locale='de_DE')[1] 

326 u'Q1' 

327 >>> get_quarter_names('narrow', locale='de_DE')[1] 

328 u'1' 

329 

330 :param width: the width to use, one of "wide", "abbreviated", or "narrow" 

331 :param context: the context, either "format" or "stand-alone" 

332 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

333 """ 

334 return Locale.parse(locale or LC_TIME).quarters[context][width] 

335 

336 

337def get_era_names( 

338 width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

339 locale: Locale | str | None = None, 

340) -> LocaleDataDict: 

341 """Return the era names used by the locale for the specified format. 

342 

343 >>> get_era_names('wide', locale='en_US')[1] 

344 u'Anno Domini' 

345 >>> get_era_names('abbreviated', locale='de_DE')[1] 

346 u'n. Chr.' 

347 

348 :param width: the width to use, either "wide", "abbreviated", or "narrow" 

349 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

350 """ 

351 return Locale.parse(locale or LC_TIME).eras[width] 

352 

353 

354def get_date_format( 

355 format: _PredefinedTimeFormat = 'medium', 

356 locale: Locale | str | None = None, 

357) -> DateTimePattern: 

358 """Return the date formatting patterns used by the locale for the specified 

359 format. 

360 

361 >>> get_date_format(locale='en_US') 

362 <DateTimePattern u'MMM d, y'> 

363 >>> get_date_format('full', locale='de_DE') 

364 <DateTimePattern u'EEEE, d. MMMM y'> 

365 

366 :param format: the format to use, one of "full", "long", "medium", or 

367 "short" 

368 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

369 """ 

370 return Locale.parse(locale or LC_TIME).date_formats[format] 

371 

372 

373def get_datetime_format( 

374 format: _PredefinedTimeFormat = 'medium', 

375 locale: Locale | str | None = None, 

376) -> DateTimePattern: 

377 """Return the datetime formatting patterns used by the locale for the 

378 specified format. 

379 

380 >>> get_datetime_format(locale='en_US') 

381 u'{1}, {0}' 

382 

383 :param format: the format to use, one of "full", "long", "medium", or 

384 "short" 

385 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

386 """ 

387 patterns = Locale.parse(locale or LC_TIME).datetime_formats 

388 if format not in patterns: 

389 format = None 

390 return patterns[format] 

391 

392 

393def get_time_format( 

394 format: _PredefinedTimeFormat = 'medium', 

395 locale: Locale | str | None = None, 

396) -> DateTimePattern: 

397 """Return the time formatting patterns used by the locale for the specified 

398 format. 

399 

400 >>> get_time_format(locale='en_US') 

401 <DateTimePattern u'h:mm:ss\u202fa'> 

402 >>> get_time_format('full', locale='de_DE') 

403 <DateTimePattern u'HH:mm:ss zzzz'> 

404 

405 :param format: the format to use, one of "full", "long", "medium", or 

406 "short" 

407 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

408 """ 

409 return Locale.parse(locale or LC_TIME).time_formats[format] 

410 

411 

412def get_timezone_gmt( 

413 datetime: _Instant = None, 

414 width: Literal['long', 'short', 'iso8601', 'iso8601_short'] = 'long', 

415 locale: Locale | str | None = None, 

416 return_z: bool = False, 

417) -> str: 

418 """Return the timezone associated with the given `datetime` object formatted 

419 as string indicating the offset from GMT. 

420 

421 >>> from datetime import datetime 

422 >>> dt = datetime(2007, 4, 1, 15, 30) 

423 >>> get_timezone_gmt(dt, locale='en') 

424 u'GMT+00:00' 

425 >>> get_timezone_gmt(dt, locale='en', return_z=True) 

426 'Z' 

427 >>> get_timezone_gmt(dt, locale='en', width='iso8601_short') 

428 u'+00' 

429 >>> tz = get_timezone('America/Los_Angeles') 

430 >>> dt = _localize(tz, datetime(2007, 4, 1, 15, 30)) 

431 >>> get_timezone_gmt(dt, locale='en') 

432 u'GMT-07:00' 

433 >>> get_timezone_gmt(dt, 'short', locale='en') 

434 u'-0700' 

435 >>> get_timezone_gmt(dt, locale='en', width='iso8601_short') 

436 u'-07' 

437 

438 The long format depends on the locale, for example in France the acronym 

439 UTC string is used instead of GMT: 

440 

441 >>> get_timezone_gmt(dt, 'long', locale='fr_FR') 

442 u'UTC-07:00' 

443 

444 .. versionadded:: 0.9 

445 

446 :param datetime: the ``datetime`` object; if `None`, the current date and 

447 time in UTC is used 

448 :param width: either "long" or "short" or "iso8601" or "iso8601_short" 

449 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

450 :param return_z: True or False; Function returns indicator "Z" 

451 when local time offset is 0 

452 """ 

453 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime)) 

454 locale = Locale.parse(locale or LC_TIME) 

455 

456 offset = datetime.tzinfo.utcoffset(datetime) 

457 seconds = offset.days * 24 * 60 * 60 + offset.seconds 

458 hours, seconds = divmod(seconds, 3600) 

459 if return_z and hours == 0 and seconds == 0: 

460 return 'Z' 

461 elif seconds == 0 and width == 'iso8601_short': 

462 return '%+03d' % hours 

463 elif width == 'short' or width == 'iso8601_short': 

464 pattern = '%+03d%02d' 

465 elif width == 'iso8601': 

466 pattern = '%+03d:%02d' 

467 else: 

468 pattern = locale.zone_formats['gmt'] % '%+03d:%02d' 

469 return pattern % (hours, seconds // 60) 

470 

471 

472def get_timezone_location( 

473 dt_or_tzinfo: _DtOrTzinfo = None, 

474 locale: Locale | str | None = None, 

475 return_city: bool = False, 

476) -> str: 

477 """Return a representation of the given timezone using "location format". 

478 

479 The result depends on both the local display name of the country and the 

480 city associated with the time zone: 

481 

482 >>> tz = get_timezone('America/St_Johns') 

483 >>> print(get_timezone_location(tz, locale='de_DE')) 

484 Kanada (St. John’s) (Ortszeit) 

485 >>> print(get_timezone_location(tz, locale='en')) 

486 Canada (St. John’s) Time 

487 >>> print(get_timezone_location(tz, locale='en', return_city=True)) 

488 St. John’s 

489 >>> tz = get_timezone('America/Mexico_City') 

490 >>> get_timezone_location(tz, locale='de_DE') 

491 u'Mexiko (Mexiko-Stadt) (Ortszeit)' 

492 

493 If the timezone is associated with a country that uses only a single 

494 timezone, just the localized country name is returned: 

495 

496 >>> tz = get_timezone('Europe/Berlin') 

497 >>> get_timezone_name(tz, locale='de_DE') 

498 u'Mitteleurop\\xe4ische Zeit' 

499 

500 .. versionadded:: 0.9 

501 

502 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines 

503 the timezone; if `None`, the current date and time in 

504 UTC is assumed 

505 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

506 :param return_city: True or False, if True then return exemplar city (location) 

507 for the time zone 

508 :return: the localized timezone name using location format 

509 

510 """ 

511 locale = Locale.parse(locale or LC_TIME) 

512 

513 zone = _get_tz_name(dt_or_tzinfo) 

514 

515 # Get the canonical time-zone code 

516 zone = get_global('zone_aliases').get(zone, zone) 

517 

518 info = locale.time_zones.get(zone, {}) 

519 

520 # Otherwise, if there is only one timezone for the country, return the 

521 # localized country name 

522 region_format = locale.zone_formats['region'] 

523 territory = get_global('zone_territories').get(zone) 

524 if territory not in locale.territories: 

525 territory = 'ZZ' # invalid/unknown 

526 territory_name = locale.territories[territory] 

527 if not return_city and territory and len(get_global('territory_zones').get(territory, [])) == 1: 

528 return region_format % territory_name 

529 

530 # Otherwise, include the city in the output 

531 fallback_format = locale.zone_formats['fallback'] 

532 if 'city' in info: 

533 city_name = info['city'] 

534 else: 

535 metazone = get_global('meta_zones').get(zone) 

536 metazone_info = locale.meta_zones.get(metazone, {}) 

537 if 'city' in metazone_info: 

538 city_name = metazone_info['city'] 

539 elif '/' in zone: 

540 city_name = zone.split('/', 1)[1].replace('_', ' ') 

541 else: 

542 city_name = zone.replace('_', ' ') 

543 

544 if return_city: 

545 return city_name 

546 return region_format % (fallback_format % { 

547 '0': city_name, 

548 '1': territory_name, 

549 }) 

550 

551 

552def get_timezone_name( 

553 dt_or_tzinfo: _DtOrTzinfo = None, 

554 width: Literal['long', 'short'] = 'long', 

555 uncommon: bool = False, 

556 locale: Locale | str | None = None, 

557 zone_variant: Literal['generic', 'daylight', 'standard'] | None = None, 

558 return_zone: bool = False, 

559) -> str: 

560 r"""Return the localized display name for the given timezone. The timezone 

561 may be specified using a ``datetime`` or `tzinfo` object. 

562 

563 >>> from datetime import time 

564 >>> dt = time(15, 30, tzinfo=get_timezone('America/Los_Angeles')) 

565 >>> get_timezone_name(dt, locale='en_US') # doctest: +SKIP 

566 u'Pacific Standard Time' 

567 >>> get_timezone_name(dt, locale='en_US', return_zone=True) 

568 'America/Los_Angeles' 

569 >>> get_timezone_name(dt, width='short', locale='en_US') # doctest: +SKIP 

570 u'PST' 

571 

572 If this function gets passed only a `tzinfo` object and no concrete 

573 `datetime`, the returned display name is independent of daylight savings 

574 time. This can be used for example for selecting timezones, or to set the 

575 time of events that recur across DST changes: 

576 

577 >>> tz = get_timezone('America/Los_Angeles') 

578 >>> get_timezone_name(tz, locale='en_US') 

579 u'Pacific Time' 

580 >>> get_timezone_name(tz, 'short', locale='en_US') 

581 u'PT' 

582 

583 If no localized display name for the timezone is available, and the timezone 

584 is associated with a country that uses only a single timezone, the name of 

585 that country is returned, formatted according to the locale: 

586 

587 >>> tz = get_timezone('Europe/Berlin') 

588 >>> get_timezone_name(tz, locale='de_DE') 

589 u'Mitteleurop\xe4ische Zeit' 

590 >>> get_timezone_name(tz, locale='pt_BR') 

591 u'Hor\xe1rio da Europa Central' 

592 

593 On the other hand, if the country uses multiple timezones, the city is also 

594 included in the representation: 

595 

596 >>> tz = get_timezone('America/St_Johns') 

597 >>> get_timezone_name(tz, locale='de_DE') 

598 u'Neufundland-Zeit' 

599 

600 Note that short format is currently not supported for all timezones and 

601 all locales. This is partially because not every timezone has a short 

602 code in every locale. In that case it currently falls back to the long 

603 format. 

604 

605 For more information see `LDML Appendix J: Time Zone Display Names 

606 <https://www.unicode.org/reports/tr35/#Time_Zone_Fallback>`_ 

607 

608 .. versionadded:: 0.9 

609 

610 .. versionchanged:: 1.0 

611 Added `zone_variant` support. 

612 

613 :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines 

614 the timezone; if a ``tzinfo`` object is used, the 

615 resulting display name will be generic, i.e. 

616 independent of daylight savings time; if `None`, the 

617 current date in UTC is assumed 

618 :param width: either "long" or "short" 

619 :param uncommon: deprecated and ignored 

620 :param zone_variant: defines the zone variation to return. By default the 

621 variation is defined from the datetime object 

622 passed in. If no datetime object is passed in, the 

623 ``'generic'`` variation is assumed. The following 

624 values are valid: ``'generic'``, ``'daylight'`` and 

625 ``'standard'``. 

626 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

627 :param return_zone: True or False. If true then function 

628 returns long time zone ID 

629 """ 

630 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) 

631 locale = Locale.parse(locale or LC_TIME) 

632 

633 zone = _get_tz_name(dt_or_tzinfo) 

634 

635 if zone_variant is None: 

636 if dt is None: 

637 zone_variant = 'generic' 

638 else: 

639 dst = tzinfo.dst(dt) 

640 zone_variant = "daylight" if dst else "standard" 

641 else: 

642 if zone_variant not in ('generic', 'standard', 'daylight'): 

643 raise ValueError('Invalid zone variation') 

644 

645 # Get the canonical time-zone code 

646 zone = get_global('zone_aliases').get(zone, zone) 

647 if return_zone: 

648 return zone 

649 info = locale.time_zones.get(zone, {}) 

650 # Try explicitly translated zone names first 

651 if width in info and zone_variant in info[width]: 

652 return info[width][zone_variant] 

653 

654 metazone = get_global('meta_zones').get(zone) 

655 if metazone: 

656 metazone_info = locale.meta_zones.get(metazone, {}) 

657 if width in metazone_info: 

658 name = metazone_info[width].get(zone_variant) 

659 if width == 'short' and name == NO_INHERITANCE_MARKER: 

660 # If the short form is marked no-inheritance, 

661 # try to fall back to the long name instead. 

662 name = metazone_info.get('long', {}).get(zone_variant) 

663 if name: 

664 return name 

665 

666 # If we have a concrete datetime, we assume that the result can't be 

667 # independent of daylight savings time, so we return the GMT offset 

668 if dt is not None: 

669 return get_timezone_gmt(dt, width=width, locale=locale) 

670 

671 return get_timezone_location(dt_or_tzinfo, locale=locale) 

672 

673 

674def format_date( 

675 date: datetime.date | None = None, 

676 format: _PredefinedTimeFormat | str = 'medium', 

677 locale: Locale | str | None = None, 

678) -> str: 

679 """Return a date formatted according to the given pattern. 

680 

681 >>> from datetime import date 

682 >>> d = date(2007, 4, 1) 

683 >>> format_date(d, locale='en_US') 

684 u'Apr 1, 2007' 

685 >>> format_date(d, format='full', locale='de_DE') 

686 u'Sonntag, 1. April 2007' 

687 

688 If you don't want to use the locale default formats, you can specify a 

689 custom date pattern: 

690 

691 >>> format_date(d, "EEE, MMM d, ''yy", locale='en') 

692 u"Sun, Apr 1, '07" 

693 

694 :param date: the ``date`` or ``datetime`` object; if `None`, the current 

695 date is used 

696 :param format: one of "full", "long", "medium", or "short", or a custom 

697 date/time pattern 

698 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale. 

699 """ 

700 if date is None: 

701 date = datetime.date.today() 

702 elif isinstance(date, datetime.datetime): 

703 date = date.date() 

704 

705 locale = Locale.parse(locale or LC_TIME) 

706 if format in ('full', 'long', 'medium', 'short'): 

707 format = get_date_format(format, locale=locale) 

708 pattern = parse_pattern(format) 

709 return pattern.apply(date, locale) 

710 

711 

712def format_datetime( 

713 datetime: _Instant = None, 

714 format: _PredefinedTimeFormat | str = 'medium', 

715 tzinfo: datetime.tzinfo | None = None, 

716 locale: Locale | str | None = None, 

717) -> str: 

718 r"""Return a date formatted according to the given pattern. 

719 

720 >>> from datetime import datetime 

721 >>> dt = datetime(2007, 4, 1, 15, 30) 

722 >>> format_datetime(dt, locale='en_US') 

723 u'Apr 1, 2007, 3:30:00\u202fPM' 

724 

725 For any pattern requiring the display of the timezone: 

726 

727 >>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'), 

728 ... locale='fr_FR') 

729 'dimanche 1 avril 2007, 17:30:00 heure d’été d’Europe centrale' 

730 >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz", 

731 ... tzinfo=get_timezone('US/Eastern'), locale='en') 

732 u'2007.04.01 AD at 11:30:00 EDT' 

733 

734 :param datetime: the `datetime` object; if `None`, the current date and 

735 time is used 

736 :param format: one of "full", "long", "medium", or "short", or a custom 

737 date/time pattern 

738 :param tzinfo: the timezone to apply to the time for display 

739 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale. 

740 """ 

741 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime), tzinfo) 

742 

743 locale = Locale.parse(locale or LC_TIME) 

744 if format in ('full', 'long', 'medium', 'short'): 

745 return get_datetime_format(format, locale=locale) \ 

746 .replace("'", "") \ 

747 .replace('{0}', format_time(datetime, format, tzinfo=None, 

748 locale=locale)) \ 

749 .replace('{1}', format_date(datetime, format, locale=locale)) 

750 else: 

751 return parse_pattern(format).apply(datetime, locale) 

752 

753 

754def format_time( 

755 time: datetime.time | datetime.datetime | float | None = None, 

756 format: _PredefinedTimeFormat | str = 'medium', 

757 tzinfo: datetime.tzinfo | None = None, 

758 locale: Locale | str | None = None, 

759) -> str: 

760 r"""Return a time formatted according to the given pattern. 

761 

762 >>> from datetime import datetime, time 

763 >>> t = time(15, 30) 

764 >>> format_time(t, locale='en_US') 

765 u'3:30:00\u202fPM' 

766 >>> format_time(t, format='short', locale='de_DE') 

767 u'15:30' 

768 

769 If you don't want to use the locale default formats, you can specify a 

770 custom time pattern: 

771 

772 >>> format_time(t, "hh 'o''clock' a", locale='en') 

773 u"03 o'clock PM" 

774 

775 For any pattern requiring the display of the time-zone a 

776 timezone has to be specified explicitly: 

777 

778 >>> t = datetime(2007, 4, 1, 15, 30) 

779 >>> tzinfo = get_timezone('Europe/Paris') 

780 >>> t = _localize(tzinfo, t) 

781 >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR') 

782 '15:30:00 heure d’été d’Europe centrale' 

783 >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'), 

784 ... locale='en') 

785 u"09 o'clock AM, Eastern Daylight Time" 

786 

787 As that example shows, when this function gets passed a 

788 ``datetime.datetime`` value, the actual time in the formatted string is 

789 adjusted to the timezone specified by the `tzinfo` parameter. If the 

790 ``datetime`` is "naive" (i.e. it has no associated timezone information), 

791 it is assumed to be in UTC. 

792 

793 These timezone calculations are **not** performed if the value is of type 

794 ``datetime.time``, as without date information there's no way to determine 

795 what a given time would translate to in a different timezone without 

796 information about whether daylight savings time is in effect or not. This 

797 means that time values are left as-is, and the value of the `tzinfo` 

798 parameter is only used to display the timezone name if needed: 

799 

800 >>> t = time(15, 30) 

801 >>> format_time(t, format='full', tzinfo=get_timezone('Europe/Paris'), 

802 ... locale='fr_FR') # doctest: +SKIP 

803 u'15:30:00 heure normale d\u2019Europe centrale' 

804 >>> format_time(t, format='full', tzinfo=get_timezone('US/Eastern'), 

805 ... locale='en_US') # doctest: +SKIP 

806 u'3:30:00\u202fPM Eastern Standard Time' 

807 

808 :param time: the ``time`` or ``datetime`` object; if `None`, the current 

809 time in UTC is used 

810 :param format: one of "full", "long", "medium", or "short", or a custom 

811 date/time pattern 

812 :param tzinfo: the time-zone to apply to the time for display 

813 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale. 

814 """ 

815 

816 # get reference date for if we need to find the right timezone variant 

817 # in the pattern 

818 ref_date = time.date() if isinstance(time, datetime.datetime) else None 

819 

820 time = _get_time(time, tzinfo) 

821 

822 locale = Locale.parse(locale or LC_TIME) 

823 if format in ('full', 'long', 'medium', 'short'): 

824 format = get_time_format(format, locale=locale) 

825 return parse_pattern(format).apply(time, locale, reference_date=ref_date) 

826 

827 

828def format_skeleton( 

829 skeleton: str, 

830 datetime: _Instant = None, 

831 tzinfo: datetime.tzinfo | None = None, 

832 fuzzy: bool = True, 

833 locale: Locale | str | None = None, 

834) -> str: 

835 r"""Return a time and/or date formatted according to the given pattern. 

836 

837 The skeletons are defined in the CLDR data and provide more flexibility 

838 than the simple short/long/medium formats, but are a bit harder to use. 

839 The are defined using the date/time symbols without order or punctuation 

840 and map to a suitable format for the given locale. 

841 

842 >>> from datetime import datetime 

843 >>> t = datetime(2007, 4, 1, 15, 30) 

844 >>> format_skeleton('MMMEd', t, locale='fr') 

845 u'dim. 1 avr.' 

846 >>> format_skeleton('MMMEd', t, locale='en') 

847 u'Sun, Apr 1' 

848 >>> format_skeleton('yMMd', t, locale='fi') # yMMd is not in the Finnish locale; yMd gets used 

849 u'1.4.2007' 

850 >>> format_skeleton('yMMd', t, fuzzy=False, locale='fi') # yMMd is not in the Finnish locale, an error is thrown 

851 Traceback (most recent call last): 

852 ... 

853 KeyError: yMMd 

854 >>> 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 

855 Traceback (most recent call last): 

856 ... 

857 KeyError: None 

858 

859 After the skeleton is resolved to a pattern `format_datetime` is called so 

860 all timezone processing etc is the same as for that. 

861 

862 :param skeleton: A date time skeleton as defined in the cldr data. 

863 :param datetime: the ``time`` or ``datetime`` object; if `None`, the current 

864 time in UTC is used 

865 :param tzinfo: the time-zone to apply to the time for display 

866 :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's 

867 close enough to it. If there is no close match, a `KeyError` 

868 is thrown. 

869 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale. 

870 """ 

871 locale = Locale.parse(locale or LC_TIME) 

872 if fuzzy and skeleton not in locale.datetime_skeletons: 

873 skeleton = match_skeleton(skeleton, locale.datetime_skeletons) 

874 format = locale.datetime_skeletons[skeleton] 

875 return format_datetime(datetime, format, tzinfo, locale) 

876 

877 

878TIMEDELTA_UNITS: tuple[tuple[str, int], ...] = ( 

879 ('year', 3600 * 24 * 365), 

880 ('month', 3600 * 24 * 30), 

881 ('week', 3600 * 24 * 7), 

882 ('day', 3600 * 24), 

883 ('hour', 3600), 

884 ('minute', 60), 

885 ('second', 1), 

886) 

887 

888 

889def format_timedelta( 

890 delta: datetime.timedelta | int, 

891 granularity: Literal['year', 'month', 'week', 'day', 'hour', 'minute', 'second'] = 'second', 

892 threshold: float = .85, 

893 add_direction: bool = False, 

894 format: Literal['narrow', 'short', 'medium', 'long'] = 'long', 

895 locale: Locale | str | None = None, 

896) -> str: 

897 """Return a time delta according to the rules of the given locale. 

898 

899 >>> from datetime import timedelta 

900 >>> format_timedelta(timedelta(weeks=12), locale='en_US') 

901 u'3 months' 

902 >>> format_timedelta(timedelta(seconds=1), locale='es') 

903 u'1 segundo' 

904 

905 The granularity parameter can be provided to alter the lowest unit 

906 presented, which defaults to a second. 

907 

908 >>> format_timedelta(timedelta(hours=3), granularity='day', locale='en_US') 

909 u'1 day' 

910 

911 The threshold parameter can be used to determine at which value the 

912 presentation switches to the next higher unit. A higher threshold factor 

913 means the presentation will switch later. For example: 

914 

915 >>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US') 

916 u'1 day' 

917 >>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US') 

918 u'23 hours' 

919 

920 In addition directional information can be provided that informs 

921 the user if the date is in the past or in the future: 

922 

923 >>> format_timedelta(timedelta(hours=1), add_direction=True, locale='en') 

924 u'in 1 hour' 

925 >>> format_timedelta(timedelta(hours=-1), add_direction=True, locale='en') 

926 u'1 hour ago' 

927 

928 The format parameter controls how compact or wide the presentation is: 

929 

930 >>> format_timedelta(timedelta(hours=3), format='short', locale='en') 

931 u'3 hr' 

932 >>> format_timedelta(timedelta(hours=3), format='narrow', locale='en') 

933 u'3h' 

934 

935 :param delta: a ``timedelta`` object representing the time difference to 

936 format, or the delta in seconds as an `int` value 

937 :param granularity: determines the smallest unit that should be displayed, 

938 the value can be one of "year", "month", "week", "day", 

939 "hour", "minute" or "second" 

940 :param threshold: factor that determines at which point the presentation 

941 switches to the next higher unit 

942 :param add_direction: if this flag is set to `True` the return value will 

943 include directional information. For instance a 

944 positive timedelta will include the information about 

945 it being in the future, a negative will be information 

946 about the value being in the past. 

947 :param format: the format, can be "narrow", "short" or "long". ( 

948 "medium" is deprecated, currently converted to "long" to 

949 maintain compatibility) 

950 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale. 

951 """ 

952 if format not in ('narrow', 'short', 'medium', 'long'): 

953 raise TypeError('Format must be one of "narrow", "short" or "long"') 

954 if format == 'medium': 

955 warnings.warn( 

956 '"medium" value for format param of format_timedelta' 

957 ' is deprecated. Use "long" instead', 

958 category=DeprecationWarning, 

959 stacklevel=2, 

960 ) 

961 format = 'long' 

962 if isinstance(delta, datetime.timedelta): 

963 seconds = int((delta.days * 86400) + delta.seconds) 

964 else: 

965 seconds = delta 

966 locale = Locale.parse(locale or LC_TIME) 

967 date_fields = locale._data["date_fields"] 

968 unit_patterns = locale._data["unit_patterns"] 

969 

970 def _iter_patterns(a_unit): 

971 if add_direction: 

972 # Try to find the length variant version first ("year-narrow") 

973 # before falling back to the default. 

974 unit_rel_patterns = (date_fields.get(f"{a_unit}-{format}") or date_fields[a_unit]) 

975 if seconds >= 0: 

976 yield unit_rel_patterns['future'] 

977 else: 

978 yield unit_rel_patterns['past'] 

979 a_unit = f"duration-{a_unit}" 

980 unit_pats = unit_patterns.get(a_unit, {}) 

981 yield unit_pats.get(format) 

982 # We do not support `<alias>` tags at all while ingesting CLDR data, 

983 # so these aliases specified in `root.xml` are hard-coded here: 

984 # <unitLength type="long"><alias source="locale" path="../unitLength[@type='short']"/></unitLength> 

985 # <unitLength type="narrow"><alias source="locale" path="../unitLength[@type='short']"/></unitLength> 

986 if format in ("long", "narrow"): 

987 yield unit_pats.get("short") 

988 

989 for unit, secs_per_unit in TIMEDELTA_UNITS: 

990 value = abs(seconds) / secs_per_unit 

991 if value >= threshold or unit == granularity: 

992 if unit == granularity and value > 0: 

993 value = max(1, value) 

994 value = int(round(value)) 

995 plural_form = locale.plural_form(value) 

996 pattern = None 

997 for patterns in _iter_patterns(unit): 

998 if patterns is not None: 

999 pattern = patterns.get(plural_form) or patterns.get('other') 

1000 if pattern: 

1001 break 

1002 # This really should not happen 

1003 if pattern is None: 

1004 return '' 

1005 return pattern.replace('{0}', str(value)) 

1006 

1007 return '' 

1008 

1009 

1010def _format_fallback_interval( 

1011 start: _Instant, 

1012 end: _Instant, 

1013 skeleton: str | None, 

1014 tzinfo: datetime.tzinfo | None, 

1015 locale: Locale, 

1016) -> str: 

1017 if skeleton in locale.datetime_skeletons: # Use the given skeleton 

1018 format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale) 

1019 elif all((isinstance(d, datetime.date) and not isinstance(d, datetime.datetime)) for d in (start, end)): # Both are just dates 

1020 format = lambda dt: format_date(dt, locale=locale) 

1021 elif all((isinstance(d, datetime.time) and not isinstance(d, datetime.date)) for d in (start, end)): # Both are times 

1022 format = lambda dt: format_time(dt, tzinfo=tzinfo, locale=locale) 

1023 else: 

1024 format = lambda dt: format_datetime(dt, tzinfo=tzinfo, locale=locale) 

1025 

1026 formatted_start = format(start) 

1027 formatted_end = format(end) 

1028 

1029 if formatted_start == formatted_end: 

1030 return format(start) 

1031 

1032 return ( 

1033 locale.interval_formats.get(None, "{0}-{1}"). 

1034 replace("{0}", formatted_start). 

1035 replace("{1}", formatted_end) 

1036 ) 

1037 

1038 

1039def format_interval( 

1040 start: _Instant, 

1041 end: _Instant, 

1042 skeleton: str | None = None, 

1043 tzinfo: datetime.tzinfo | None = None, 

1044 fuzzy: bool = True, 

1045 locale: Locale | str | None = None, 

1046) -> str: 

1047 """ 

1048 Format an interval between two instants according to the locale's rules. 

1049 

1050 >>> from datetime import date, time 

1051 >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "yMd", locale="fi") 

1052 u'15.\u201317.1.2016' 

1053 

1054 >>> format_interval(time(12, 12), time(16, 16), "Hm", locale="en_GB") 

1055 '12:12\u201316:16' 

1056 

1057 >>> format_interval(time(5, 12), time(16, 16), "hm", locale="en_US") 

1058 '5:12\u202fAM\u2009–\u20094:16\u202fPM' 

1059 

1060 >>> format_interval(time(16, 18), time(16, 24), "Hm", locale="it") 

1061 '16:18\u201316:24' 

1062 

1063 If the start instant equals the end instant, the interval is formatted like the instant. 

1064 

1065 >>> format_interval(time(16, 18), time(16, 18), "Hm", locale="it") 

1066 '16:18' 

1067 

1068 Unknown skeletons fall back to "default" formatting. 

1069 

1070 >>> format_interval(date(2015, 1, 1), date(2017, 1, 1), "wzq", locale="ja") 

1071 '2015/01/01\uff5e2017/01/01' 

1072 

1073 >>> format_interval(time(16, 18), time(16, 24), "xxx", locale="ja") 

1074 '16:18:00\uff5e16:24:00' 

1075 

1076 >>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "xxx", locale="de") 

1077 '15.01.2016\u2009–\u200917.01.2016' 

1078 

1079 :param start: First instant (datetime/date/time) 

1080 :param end: Second instant (datetime/date/time) 

1081 :param skeleton: The "skeleton format" to use for formatting. 

1082 :param tzinfo: tzinfo to use (if none is already attached) 

1083 :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's 

1084 close enough to it. 

1085 :param locale: A locale object or identifier. Defaults to the system time locale. 

1086 :return: Formatted interval 

1087 """ 

1088 locale = Locale.parse(locale or LC_TIME) 

1089 

1090 # NB: The quote comments below are from the algorithm description in 

1091 # https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats 

1092 

1093 # > Look for the intervalFormatItem element that matches the "skeleton", 

1094 # > starting in the current locale and then following the locale fallback 

1095 # > chain up to, but not including root. 

1096 

1097 interval_formats = locale.interval_formats 

1098 

1099 if skeleton not in interval_formats or not skeleton: 

1100 # > If no match was found from the previous step, check what the closest 

1101 # > match is in the fallback locale chain, as in availableFormats. That 

1102 # > is, this allows for adjusting the string value field's width, 

1103 # > including adjusting between "MMM" and "MMMM", and using different 

1104 # > variants of the same field, such as 'v' and 'z'. 

1105 if skeleton and fuzzy: 

1106 skeleton = match_skeleton(skeleton, interval_formats) 

1107 else: 

1108 skeleton = None 

1109 if not skeleton: # Still no match whatsoever? 

1110 # > Otherwise, format the start and end datetime using the fallback pattern. 

1111 return _format_fallback_interval(start, end, skeleton, tzinfo, locale) 

1112 

1113 skel_formats = interval_formats[skeleton] 

1114 

1115 if start == end: 

1116 return format_skeleton(skeleton, start, tzinfo, fuzzy=fuzzy, locale=locale) 

1117 

1118 start = _ensure_datetime_tzinfo(_get_datetime(start), tzinfo=tzinfo) 

1119 end = _ensure_datetime_tzinfo(_get_datetime(end), tzinfo=tzinfo) 

1120 

1121 start_fmt = DateTimeFormat(start, locale=locale) 

1122 end_fmt = DateTimeFormat(end, locale=locale) 

1123 

1124 # > If a match is found from previous steps, compute the calendar field 

1125 # > with the greatest difference between start and end datetime. If there 

1126 # > is no difference among any of the fields in the pattern, format as a 

1127 # > single date using availableFormats, and return. 

1128 

1129 for field in PATTERN_CHAR_ORDER: # These are in largest-to-smallest order 

1130 if field in skel_formats and start_fmt.extract(field) != end_fmt.extract(field): 

1131 # > If there is a match, use the pieces of the corresponding pattern to 

1132 # > format the start and end datetime, as above. 

1133 return "".join( 

1134 parse_pattern(pattern).apply(instant, locale) 

1135 for pattern, instant 

1136 in zip(skel_formats[field], (start, end)) 

1137 ) 

1138 

1139 # > Otherwise, format the start and end datetime using the fallback pattern. 

1140 

1141 return _format_fallback_interval(start, end, skeleton, tzinfo, locale) 

1142 

1143 

1144def get_period_id( 

1145 time: _Instant, 

1146 tzinfo: datetime.tzinfo | None = None, 

1147 type: Literal['selection'] | None = None, 

1148 locale: Locale | str | None = None, 

1149) -> str: 

1150 """ 

1151 Get the day period ID for a given time. 

1152 

1153 This ID can be used as a key for the period name dictionary. 

1154 

1155 >>> from datetime import time 

1156 >>> get_period_names(locale="de")[get_period_id(time(7, 42), locale="de")] 

1157 u'Morgen' 

1158 

1159 >>> get_period_id(time(0), locale="en_US") 

1160 u'midnight' 

1161 

1162 >>> get_period_id(time(0), type="selection", locale="en_US") 

1163 u'night1' 

1164 

1165 :param time: The time to inspect. 

1166 :param tzinfo: The timezone for the time. See ``format_time``. 

1167 :param type: The period type to use. Either "selection" or None. 

1168 The selection type is used for selecting among phrases such as 

1169 “Your email arrived yesterday evening” or “Your email arrived last night”. 

1170 :param locale: the `Locale` object, or a locale string. Defaults to the system time locale. 

1171 :return: period ID. Something is always returned -- even if it's just "am" or "pm". 

1172 """ 

1173 time = _get_time(time, tzinfo) 

1174 seconds_past_midnight = int(time.hour * 60 * 60 + time.minute * 60 + time.second) 

1175 locale = Locale.parse(locale or LC_TIME) 

1176 

1177 # The LDML rules state that the rules may not overlap, so iterating in arbitrary 

1178 # order should be alright, though `at` periods should be preferred. 

1179 rulesets = locale.day_period_rules.get(type, {}).items() 

1180 

1181 for rule_id, rules in rulesets: 

1182 for rule in rules: 

1183 if "at" in rule and rule["at"] == seconds_past_midnight: 

1184 return rule_id 

1185 

1186 for rule_id, rules in rulesets: 

1187 for rule in rules: 

1188 if "from" in rule and "before" in rule: 

1189 if rule["from"] < rule["before"]: 

1190 if rule["from"] <= seconds_past_midnight < rule["before"]: 

1191 return rule_id 

1192 else: 

1193 # e.g. from="21:00" before="06:00" 

1194 if rule["from"] <= seconds_past_midnight < 86400 or \ 

1195 0 <= seconds_past_midnight < rule["before"]: 

1196 return rule_id 

1197 

1198 start_ok = end_ok = False 

1199 

1200 if "from" in rule and seconds_past_midnight >= rule["from"]: 

1201 start_ok = True 

1202 if "to" in rule and seconds_past_midnight <= rule["to"]: 

1203 # This rule type does not exist in the present CLDR data; 

1204 # excuse the lack of test coverage. 

1205 end_ok = True 

1206 if "before" in rule and seconds_past_midnight < rule["before"]: 

1207 end_ok = True 

1208 if "after" in rule: 

1209 raise NotImplementedError("'after' is deprecated as of CLDR 29.") 

1210 

1211 if start_ok and end_ok: 

1212 return rule_id 

1213 

1214 if seconds_past_midnight < 43200: 

1215 return "am" 

1216 else: 

1217 return "pm" 

1218 

1219 

1220class ParseError(ValueError): 

1221 pass 

1222 

1223 

1224def parse_date( 

1225 string: str, 

1226 locale: Locale | str | None = None, 

1227 format: _PredefinedTimeFormat | str = 'medium', 

1228) -> datetime.date: 

1229 """Parse a date from a string. 

1230 

1231 If an explicit format is provided, it is used to parse the date. 

1232 

1233 >>> parse_date('01.04.2004', format='dd.MM.yyyy') 

1234 datetime.date(2004, 4, 1) 

1235 

1236 If no format is given, or if it is one of "full", "long", "medium", 

1237 or "short", the function first tries to interpret the string as 

1238 ISO-8601 date format and then uses the date format for the locale 

1239 as a hint to determine the order in which the date fields appear in 

1240 the string. 

1241 

1242 >>> parse_date('4/1/04', locale='en_US') 

1243 datetime.date(2004, 4, 1) 

1244 >>> parse_date('01.04.2004', locale='de_DE') 

1245 datetime.date(2004, 4, 1) 

1246 >>> parse_date('2004-04-01', locale='en_US') 

1247 datetime.date(2004, 4, 1) 

1248 >>> parse_date('2004-04-01', locale='de_DE') 

1249 datetime.date(2004, 4, 1) 

1250 >>> parse_date('01.04.04', locale='de_DE', format='short') 

1251 datetime.date(2004, 4, 1) 

1252 

1253 :param string: the string containing the date 

1254 :param locale: a `Locale` object or a locale identifier 

1255 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale. 

1256 :param format: the format to use, either an explicit date format, 

1257 or one of "full", "long", "medium", or "short" 

1258 (see ``get_time_format``) 

1259 """ 

1260 numbers = re.findall(r'(\d+)', string) 

1261 if not numbers: 

1262 raise ParseError("No numbers were found in input") 

1263 

1264 use_predefined_format = format in ('full', 'long', 'medium', 'short') 

1265 # we try ISO-8601 format first, meaning similar to formats 

1266 # extended YYYY-MM-DD or basic YYYYMMDD 

1267 iso_alike = re.match(r'^(\d{4})-?([01]\d)-?([0-3]\d)$', 

1268 string, flags=re.ASCII) # allow only ASCII digits 

1269 if iso_alike and use_predefined_format: 

1270 try: 

1271 return datetime.date(*map(int, iso_alike.groups())) 

1272 except ValueError: 

1273 pass # a locale format might fit better, so let's continue 

1274 

1275 if use_predefined_format: 

1276 fmt = get_date_format(format=format, locale=locale) 

1277 else: 

1278 fmt = parse_pattern(format) 

1279 format_str = fmt.pattern.lower() 

1280 year_idx = format_str.index('y') 

1281 month_idx = format_str.find('m') 

1282 if month_idx < 0: 

1283 month_idx = format_str.index('l') 

1284 day_idx = format_str.index('d') 

1285 

1286 indexes = sorted([(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')]) 

1287 indexes = {item[1]: idx for idx, item in enumerate(indexes)} 

1288 

1289 # FIXME: this currently only supports numbers, but should also support month 

1290 # names, both in the requested locale, and english 

1291 

1292 year = numbers[indexes['Y']] 

1293 year = 2000 + int(year) if len(year) == 2 else int(year) 

1294 month = int(numbers[indexes['M']]) 

1295 day = int(numbers[indexes['D']]) 

1296 if month > 12: 

1297 month, day = day, month 

1298 return datetime.date(year, month, day) 

1299 

1300 

1301def parse_time( 

1302 string: str, 

1303 locale: Locale | str | None = None, 

1304 format: _PredefinedTimeFormat | str = 'medium', 

1305) -> datetime.time: 

1306 """Parse a time from a string. 

1307 

1308 This function uses the time format for the locale as a hint to determine 

1309 the order in which the time fields appear in the string. 

1310 

1311 If an explicit format is provided, the function will use it to parse 

1312 the time instead. 

1313 

1314 >>> parse_time('15:30:00', locale='en_US') 

1315 datetime.time(15, 30) 

1316 >>> parse_time('15:30:00', format='H:mm:ss') 

1317 datetime.time(15, 30) 

1318 

1319 :param string: the string containing the time 

1320 :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale. 

1321 :param format: the format to use, either an explicit time format, 

1322 or one of "full", "long", "medium", or "short" 

1323 (see ``get_time_format``) 

1324 :return: the parsed time 

1325 :rtype: `time` 

1326 """ 

1327 numbers = re.findall(r'(\d+)', string) 

1328 if not numbers: 

1329 raise ParseError("No numbers were found in input") 

1330 

1331 # TODO: try ISO format first? 

1332 if format in ('full', 'long', 'medium', 'short'): 

1333 fmt = get_time_format(format=format, locale=locale) 

1334 else: 

1335 fmt = parse_pattern(format) 

1336 format_str = fmt.pattern.lower() 

1337 hour_idx = format_str.find('h') 

1338 if hour_idx < 0: 

1339 hour_idx = format_str.index('k') 

1340 min_idx = format_str.index('m') 

1341 # format might not contain seconds 

1342 if (sec_idx := format_str.find('s')) < 0: 

1343 sec_idx = math.inf 

1344 

1345 indexes = sorted([(hour_idx, 'H'), (min_idx, 'M'), (sec_idx, 'S')]) 

1346 indexes = {item[1]: idx for idx, item in enumerate(indexes)} 

1347 

1348 # TODO: support time zones 

1349 

1350 # Check if the format specifies a period to be used; 

1351 # if it does, look for 'pm' to figure out an offset. 

1352 hour_offset = 0 

1353 if 'a' in format_str and 'pm' in string.lower(): 

1354 hour_offset = 12 

1355 

1356 # Parse up to three numbers from the string. 

1357 minute = second = 0 

1358 hour = int(numbers[indexes['H']]) + hour_offset 

1359 if len(numbers) > 1: 

1360 minute = int(numbers[indexes['M']]) 

1361 if len(numbers) > 2: 

1362 second = int(numbers[indexes['S']]) 

1363 return datetime.time(hour, minute, second) 

1364 

1365 

1366class DateTimePattern: 

1367 

1368 def __init__(self, pattern: str, format: DateTimeFormat): 

1369 self.pattern = pattern 

1370 self.format = format 

1371 

1372 def __repr__(self) -> str: 

1373 return f"<{type(self).__name__} {self.pattern!r}>" 

1374 

1375 def __str__(self) -> str: 

1376 pat = self.pattern 

1377 return pat 

1378 

1379 def __mod__(self, other: DateTimeFormat) -> str: 

1380 if not isinstance(other, DateTimeFormat): 

1381 return NotImplemented 

1382 return self.format % other 

1383 

1384 def apply( 

1385 self, 

1386 datetime: datetime.date | datetime.time, 

1387 locale: Locale | str | None, 

1388 reference_date: datetime.date | None = None, 

1389 ) -> str: 

1390 return self % DateTimeFormat(datetime, locale, reference_date) 

1391 

1392 

1393class DateTimeFormat: 

1394 

1395 def __init__( 

1396 self, 

1397 value: datetime.date | datetime.time, 

1398 locale: Locale | str, 

1399 reference_date: datetime.date | None = None, 

1400 ) -> None: 

1401 assert isinstance(value, (datetime.date, datetime.datetime, datetime.time)) 

1402 if isinstance(value, (datetime.datetime, datetime.time)) and value.tzinfo is None: 

1403 value = value.replace(tzinfo=UTC) 

1404 self.value = value 

1405 self.locale = Locale.parse(locale) 

1406 self.reference_date = reference_date 

1407 

1408 def __getitem__(self, name: str) -> str: 

1409 char = name[0] 

1410 num = len(name) 

1411 if char == 'G': 

1412 return self.format_era(char, num) 

1413 elif char in ('y', 'Y', 'u'): 

1414 return self.format_year(char, num) 

1415 elif char in ('Q', 'q'): 

1416 return self.format_quarter(char, num) 

1417 elif char in ('M', 'L'): 

1418 return self.format_month(char, num) 

1419 elif char in ('w', 'W'): 

1420 return self.format_week(char, num) 

1421 elif char == 'd': 

1422 return self.format(self.value.day, num) 

1423 elif char == 'D': 

1424 return self.format_day_of_year(num) 

1425 elif char == 'F': 

1426 return self.format_day_of_week_in_month() 

1427 elif char in ('E', 'e', 'c'): 

1428 return self.format_weekday(char, num) 

1429 elif char in ('a', 'b', 'B'): 

1430 return self.format_period(char, num) 

1431 elif char == 'h': 

1432 if self.value.hour % 12 == 0: 

1433 return self.format(12, num) 

1434 else: 

1435 return self.format(self.value.hour % 12, num) 

1436 elif char == 'H': 

1437 return self.format(self.value.hour, num) 

1438 elif char == 'K': 

1439 return self.format(self.value.hour % 12, num) 

1440 elif char == 'k': 

1441 if self.value.hour == 0: 

1442 return self.format(24, num) 

1443 else: 

1444 return self.format(self.value.hour, num) 

1445 elif char == 'm': 

1446 return self.format(self.value.minute, num) 

1447 elif char == 's': 

1448 return self.format(self.value.second, num) 

1449 elif char == 'S': 

1450 return self.format_frac_seconds(num) 

1451 elif char == 'A': 

1452 return self.format_milliseconds_in_day(num) 

1453 elif char in ('z', 'Z', 'v', 'V', 'x', 'X', 'O'): 

1454 return self.format_timezone(char, num) 

1455 else: 

1456 raise KeyError(f"Unsupported date/time field {char!r}") 

1457 

1458 def extract(self, char: str) -> int: 

1459 char = str(char)[0] 

1460 if char == 'y': 

1461 return self.value.year 

1462 elif char == 'M': 

1463 return self.value.month 

1464 elif char == 'd': 

1465 return self.value.day 

1466 elif char == 'H': 

1467 return self.value.hour 

1468 elif char == 'h': 

1469 return self.value.hour % 12 or 12 

1470 elif char == 'm': 

1471 return self.value.minute 

1472 elif char == 'a': 

1473 return int(self.value.hour >= 12) # 0 for am, 1 for pm 

1474 else: 

1475 raise NotImplementedError(f"Not implemented: extracting {char!r} from {self.value!r}") 

1476 

1477 def format_era(self, char: str, num: int) -> str: 

1478 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] 

1479 era = int(self.value.year >= 0) 

1480 return get_era_names(width, self.locale)[era] 

1481 

1482 def format_year(self, char: str, num: int) -> str: 

1483 value = self.value.year 

1484 if char.isupper(): 

1485 month = self.value.month 

1486 if month == 1 and self.value.day < 7 and self.get_week_of_year() >= 52: 

1487 value -= 1 

1488 elif month == 12 and self.value.day > 25 and self.get_week_of_year() <= 2: 

1489 value += 1 

1490 year = self.format(value, num) 

1491 if num == 2: 

1492 year = year[-2:] 

1493 return year 

1494 

1495 def format_quarter(self, char: str, num: int) -> str: 

1496 quarter = (self.value.month - 1) // 3 + 1 

1497 if num <= 2: 

1498 return '%0*d' % (num, quarter) 

1499 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] 

1500 context = {'Q': 'format', 'q': 'stand-alone'}[char] 

1501 return get_quarter_names(width, context, self.locale)[quarter] 

1502 

1503 def format_month(self, char: str, num: int) -> str: 

1504 if num <= 2: 

1505 return '%0*d' % (num, self.value.month) 

1506 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num] 

1507 context = {'M': 'format', 'L': 'stand-alone'}[char] 

1508 return get_month_names(width, context, self.locale)[self.value.month] 

1509 

1510 def format_week(self, char: str, num: int) -> str: 

1511 if char.islower(): # week of year 

1512 week = self.get_week_of_year() 

1513 return self.format(week, num) 

1514 else: # week of month 

1515 week = self.get_week_of_month() 

1516 return str(week) 

1517 

1518 def format_weekday(self, char: str = 'E', num: int = 4) -> str: 

1519 """ 

1520 Return weekday from parsed datetime according to format pattern. 

1521 

1522 >>> from datetime import date 

1523 >>> format = DateTimeFormat(date(2016, 2, 28), Locale.parse('en_US')) 

1524 >>> format.format_weekday() 

1525 u'Sunday' 

1526 

1527 'E': Day of week - Use one through three letters for the abbreviated day name, four for the full (wide) name, 

1528 five for the narrow name, or six for the short name. 

1529 >>> format.format_weekday('E',2) 

1530 u'Sun' 

1531 

1532 'e': Local day of week. Same as E except adds a numeric value that will depend on the local starting day of the 

1533 week, using one or two letters. For this example, Monday is the first day of the week. 

1534 >>> format.format_weekday('e',2) 

1535 '01' 

1536 

1537 'c': Stand-Alone local day of week - Use one letter for the local numeric value (same as 'e'), three for the 

1538 abbreviated day name, four for the full (wide) name, five for the narrow name, or six for the short name. 

1539 >>> format.format_weekday('c',1) 

1540 '1' 

1541 

1542 :param char: pattern format character ('e','E','c') 

1543 :param num: count of format character 

1544 

1545 """ 

1546 if num < 3: 

1547 if char.islower(): 

1548 value = 7 - self.locale.first_week_day + self.value.weekday() 

1549 return self.format(value % 7 + 1, num) 

1550 num = 3 

1551 weekday = self.value.weekday() 

1552 width = {3: 'abbreviated', 4: 'wide', 5: 'narrow', 6: 'short'}[num] 

1553 context = "stand-alone" if char == "c" else "format" 

1554 return get_day_names(width, context, self.locale)[weekday] 

1555 

1556 def format_day_of_year(self, num: int) -> str: 

1557 return self.format(self.get_day_of_year(), num) 

1558 

1559 def format_day_of_week_in_month(self) -> str: 

1560 return str((self.value.day - 1) // 7 + 1) 

1561 

1562 def format_period(self, char: str, num: int) -> str: 

1563 """ 

1564 Return period from parsed datetime according to format pattern. 

1565 

1566 >>> from datetime import datetime, time 

1567 >>> format = DateTimeFormat(time(13, 42), 'fi_FI') 

1568 >>> format.format_period('a', 1) 

1569 u'ip.' 

1570 >>> format.format_period('b', 1) 

1571 u'iltap.' 

1572 >>> format.format_period('b', 4) 

1573 u'iltapäivä' 

1574 >>> format.format_period('B', 4) 

1575 u'iltapäivällä' 

1576 >>> format.format_period('B', 5) 

1577 u'ip.' 

1578 

1579 >>> format = DateTimeFormat(datetime(2022, 4, 28, 6, 27), 'zh_Hant') 

1580 >>> format.format_period('a', 1) 

1581 u'上午' 

1582 >>> format.format_period('B', 1) 

1583 u'清晨' 

1584 

1585 :param char: pattern format character ('a', 'b', 'B') 

1586 :param num: count of format character 

1587 

1588 """ 

1589 widths = [{3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)], 

1590 'wide', 'narrow', 'abbreviated'] 

1591 if char == 'a': 

1592 period = 'pm' if self.value.hour >= 12 else 'am' 

1593 context = 'format' 

1594 else: 

1595 period = get_period_id(self.value, locale=self.locale) 

1596 context = 'format' if char == 'B' else 'stand-alone' 

1597 for width in widths: 

1598 period_names = get_period_names(context=context, width=width, locale=self.locale) 

1599 if period in period_names: 

1600 return period_names[period] 

1601 raise ValueError(f"Could not format period {period} in {self.locale}") 

1602 

1603 def format_frac_seconds(self, num: int) -> str: 

1604 """ Return fractional seconds. 

1605 

1606 Rounds the time's microseconds to the precision given by the number \ 

1607 of digits passed in. 

1608 """ 

1609 value = self.value.microsecond / 1000000 

1610 return self.format(round(value, num) * 10**num, num) 

1611 

1612 def format_milliseconds_in_day(self, num): 

1613 msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \ 

1614 self.value.minute * 60000 + self.value.hour * 3600000 

1615 return self.format(msecs, num) 

1616 

1617 def format_timezone(self, char: str, num: int) -> str: 

1618 width = {3: 'short', 4: 'long', 5: 'iso8601'}[max(3, num)] 

1619 

1620 # It could be that we only receive a time to format, but also have a 

1621 # reference date which is important to distinguish between timezone 

1622 # variants (summer/standard time) 

1623 value = self.value 

1624 if self.reference_date: 

1625 value = datetime.datetime.combine(self.reference_date, self.value) 

1626 

1627 if char == 'z': 

1628 return get_timezone_name(value, width, locale=self.locale) 

1629 elif char == 'Z': 

1630 if num == 5: 

1631 return get_timezone_gmt(value, width, locale=self.locale, return_z=True) 

1632 return get_timezone_gmt(value, width, locale=self.locale) 

1633 elif char == 'O': 

1634 if num == 4: 

1635 return get_timezone_gmt(value, width, locale=self.locale) 

1636 # TODO: To add support for O:1 

1637 elif char == 'v': 

1638 return get_timezone_name(value.tzinfo, width, 

1639 locale=self.locale) 

1640 elif char == 'V': 

1641 if num == 1: 

1642 return get_timezone_name(value.tzinfo, width, 

1643 uncommon=True, locale=self.locale) 

1644 elif num == 2: 

1645 return get_timezone_name(value.tzinfo, locale=self.locale, return_zone=True) 

1646 elif num == 3: 

1647 return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True) 

1648 return get_timezone_location(value.tzinfo, locale=self.locale) 

1649 # Included additional elif condition to add support for 'Xx' in timezone format 

1650 elif char == 'X': 

1651 if num == 1: 

1652 return get_timezone_gmt(value, width='iso8601_short', locale=self.locale, 

1653 return_z=True) 

1654 elif num in (2, 4): 

1655 return get_timezone_gmt(value, width='short', locale=self.locale, 

1656 return_z=True) 

1657 elif num in (3, 5): 

1658 return get_timezone_gmt(value, width='iso8601', locale=self.locale, 

1659 return_z=True) 

1660 elif char == 'x': 

1661 if num == 1: 

1662 return get_timezone_gmt(value, width='iso8601_short', locale=self.locale) 

1663 elif num in (2, 4): 

1664 return get_timezone_gmt(value, width='short', locale=self.locale) 

1665 elif num in (3, 5): 

1666 return get_timezone_gmt(value, width='iso8601', locale=self.locale) 

1667 

1668 def format(self, value: SupportsInt, length: int) -> str: 

1669 return '%0*d' % (length, value) 

1670 

1671 def get_day_of_year(self, date: datetime.date | None = None) -> int: 

1672 if date is None: 

1673 date = self.value 

1674 return (date - date.replace(month=1, day=1)).days + 1 

1675 

1676 def get_week_of_year(self) -> int: 

1677 """Return the week of the year.""" 

1678 day_of_year = self.get_day_of_year(self.value) 

1679 week = self.get_week_number(day_of_year) 

1680 if week == 0: 

1681 date = datetime.date(self.value.year - 1, 12, 31) 

1682 week = self.get_week_number(self.get_day_of_year(date), 

1683 date.weekday()) 

1684 elif week > 52: 

1685 weekday = datetime.date(self.value.year + 1, 1, 1).weekday() 

1686 if self.get_week_number(1, weekday) == 1 and \ 

1687 32 - (weekday - self.locale.first_week_day) % 7 <= self.value.day: 

1688 week = 1 

1689 return week 

1690 

1691 def get_week_of_month(self) -> int: 

1692 """Return the week of the month.""" 

1693 return self.get_week_number(self.value.day) 

1694 

1695 def get_week_number(self, day_of_period: int, day_of_week: int | None = None) -> int: 

1696 """Return the number of the week of a day within a period. This may be 

1697 the week number in a year or the week number in a month. 

1698 

1699 Usually this will return a value equal to or greater than 1, but if the 

1700 first week of the period is so short that it actually counts as the last 

1701 week of the previous period, this function will return 0. 

1702 

1703 >>> date = datetime.date(2006, 1, 8) 

1704 >>> DateTimeFormat(date, 'de_DE').get_week_number(6) 

1705 1 

1706 >>> DateTimeFormat(date, 'en_US').get_week_number(6) 

1707 2 

1708 

1709 :param day_of_period: the number of the day in the period (usually 

1710 either the day of month or the day of year) 

1711 :param day_of_week: the week day; if omitted, the week day of the 

1712 current date is assumed 

1713 """ 

1714 if day_of_week is None: 

1715 day_of_week = self.value.weekday() 

1716 first_day = (day_of_week - self.locale.first_week_day - 

1717 day_of_period + 1) % 7 

1718 if first_day < 0: 

1719 first_day += 7 

1720 week_number = (day_of_period + first_day - 1) // 7 

1721 if 7 - first_day >= self.locale.min_week_days: 

1722 week_number += 1 

1723 return week_number 

1724 

1725 

1726PATTERN_CHARS: dict[str, list[int] | None] = { 

1727 'G': [1, 2, 3, 4, 5], # era 

1728 'y': None, 'Y': None, 'u': None, # year 

1729 'Q': [1, 2, 3, 4, 5], 'q': [1, 2, 3, 4, 5], # quarter 

1730 'M': [1, 2, 3, 4, 5], 'L': [1, 2, 3, 4, 5], # month 

1731 'w': [1, 2], 'W': [1], # week 

1732 'd': [1, 2], 'D': [1, 2, 3], 'F': [1], 'g': None, # day 

1733 'E': [1, 2, 3, 4, 5, 6], 'e': [1, 2, 3, 4, 5, 6], 'c': [1, 3, 4, 5, 6], # week day 

1734 'a': [1, 2, 3, 4, 5], 'b': [1, 2, 3, 4, 5], 'B': [1, 2, 3, 4, 5], # period 

1735 'h': [1, 2], 'H': [1, 2], 'K': [1, 2], 'k': [1, 2], # hour 

1736 'm': [1, 2], # minute 

1737 's': [1, 2], 'S': None, 'A': None, # second 

1738 'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4, 5], 'O': [1, 4], 'v': [1, 4], # zone 

1739 'V': [1, 2, 3, 4], 'x': [1, 2, 3, 4, 5], 'X': [1, 2, 3, 4, 5], # zone 

1740} 

1741 

1742#: The pattern characters declared in the Date Field Symbol Table 

1743#: (https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) 

1744#: in order of decreasing magnitude. 

1745PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" 

1746 

1747 

1748def parse_pattern(pattern: str | DateTimePattern) -> DateTimePattern: 

1749 """Parse date, time, and datetime format patterns. 

1750 

1751 >>> parse_pattern("MMMMd").format 

1752 u'%(MMMM)s%(d)s' 

1753 >>> parse_pattern("MMM d, yyyy").format 

1754 u'%(MMM)s %(d)s, %(yyyy)s' 

1755 

1756 Pattern can contain literal strings in single quotes: 

1757 

1758 >>> parse_pattern("H:mm' Uhr 'z").format 

1759 u'%(H)s:%(mm)s Uhr %(z)s' 

1760 

1761 An actual single quote can be used by using two adjacent single quote 

1762 characters: 

1763 

1764 >>> parse_pattern("hh' o''clock'").format 

1765 u"%(hh)s o'clock" 

1766 

1767 :param pattern: the formatting pattern to parse 

1768 """ 

1769 if isinstance(pattern, DateTimePattern): 

1770 return pattern 

1771 return _cached_parse_pattern(pattern) 

1772 

1773 

1774@lru_cache(maxsize=1024) 

1775def _cached_parse_pattern(pattern: str) -> DateTimePattern: 

1776 result = [] 

1777 

1778 for tok_type, tok_value in tokenize_pattern(pattern): 

1779 if tok_type == "chars": 

1780 result.append(tok_value.replace('%', '%%')) 

1781 elif tok_type == "field": 

1782 fieldchar, fieldnum = tok_value 

1783 limit = PATTERN_CHARS[fieldchar] 

1784 if limit and fieldnum not in limit: 

1785 raise ValueError(f"Invalid length for field: {fieldchar * fieldnum!r}") 

1786 result.append('%%(%s)s' % (fieldchar * fieldnum)) 

1787 else: 

1788 raise NotImplementedError(f"Unknown token type: {tok_type}") 

1789 return DateTimePattern(pattern, ''.join(result)) 

1790 

1791 

1792def tokenize_pattern(pattern: str) -> list[tuple[str, str | tuple[str, int]]]: 

1793 """ 

1794 Tokenize date format patterns. 

1795 

1796 Returns a list of (token_type, token_value) tuples. 

1797 

1798 ``token_type`` may be either "chars" or "field". 

1799 

1800 For "chars" tokens, the value is the literal value. 

1801 

1802 For "field" tokens, the value is a tuple of (field character, repetition count). 

1803 

1804 :param pattern: Pattern string 

1805 :type pattern: str 

1806 :rtype: list[tuple] 

1807 """ 

1808 result = [] 

1809 quotebuf = None 

1810 charbuf = [] 

1811 fieldchar = [''] 

1812 fieldnum = [0] 

1813 

1814 def append_chars(): 

1815 result.append(('chars', ''.join(charbuf).replace('\0', "'"))) 

1816 del charbuf[:] 

1817 

1818 def append_field(): 

1819 result.append(('field', (fieldchar[0], fieldnum[0]))) 

1820 fieldchar[0] = '' 

1821 fieldnum[0] = 0 

1822 

1823 for char in pattern.replace("''", '\0'): 

1824 if quotebuf is None: 

1825 if char == "'": # quote started 

1826 if fieldchar[0]: 

1827 append_field() 

1828 elif charbuf: 

1829 append_chars() 

1830 quotebuf = [] 

1831 elif char in PATTERN_CHARS: 

1832 if charbuf: 

1833 append_chars() 

1834 if char == fieldchar[0]: 

1835 fieldnum[0] += 1 

1836 else: 

1837 if fieldchar[0]: 

1838 append_field() 

1839 fieldchar[0] = char 

1840 fieldnum[0] = 1 

1841 else: 

1842 if fieldchar[0]: 

1843 append_field() 

1844 charbuf.append(char) 

1845 

1846 elif quotebuf is not None: 

1847 if char == "'": # end of quote 

1848 charbuf.extend(quotebuf) 

1849 quotebuf = None 

1850 else: # inside quote 

1851 quotebuf.append(char) 

1852 

1853 if fieldchar[0]: 

1854 append_field() 

1855 elif charbuf: 

1856 append_chars() 

1857 

1858 return result 

1859 

1860 

1861def untokenize_pattern(tokens: Iterable[tuple[str, str | tuple[str, int]]]) -> str: 

1862 """ 

1863 Turn a date format pattern token stream back into a string. 

1864 

1865 This is the reverse operation of ``tokenize_pattern``. 

1866 

1867 :type tokens: Iterable[tuple] 

1868 :rtype: str 

1869 """ 

1870 output = [] 

1871 for tok_type, tok_value in tokens: 

1872 if tok_type == "field": 

1873 output.append(tok_value[0] * tok_value[1]) 

1874 elif tok_type == "chars": 

1875 if not any(ch in PATTERN_CHARS for ch in tok_value): # No need to quote 

1876 output.append(tok_value) 

1877 else: 

1878 output.append("'%s'" % tok_value.replace("'", "''")) 

1879 return "".join(output) 

1880 

1881 

1882def split_interval_pattern(pattern: str) -> list[str]: 

1883 """ 

1884 Split an interval-describing datetime pattern into multiple pieces. 

1885 

1886 > The pattern is then designed to be broken up into two pieces by determining the first repeating field. 

1887 - https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats 

1888 

1889 >>> split_interval_pattern(u'E d.M. \u2013 E d.M.') 

1890 [u'E d.M. \u2013 ', 'E d.M.'] 

1891 >>> split_interval_pattern("Y 'text' Y 'more text'") 

1892 ["Y 'text '", "Y 'more text'"] 

1893 >>> split_interval_pattern(u"E, MMM d \u2013 E") 

1894 [u'E, MMM d \u2013 ', u'E'] 

1895 >>> split_interval_pattern("MMM d") 

1896 ['MMM d'] 

1897 >>> split_interval_pattern("y G") 

1898 ['y G'] 

1899 >>> split_interval_pattern(u"MMM d \u2013 d") 

1900 [u'MMM d \u2013 ', u'd'] 

1901 

1902 :param pattern: Interval pattern string 

1903 :return: list of "subpatterns" 

1904 """ 

1905 

1906 seen_fields = set() 

1907 parts = [[]] 

1908 

1909 for tok_type, tok_value in tokenize_pattern(pattern): 

1910 if tok_type == "field": 

1911 if tok_value[0] in seen_fields: # Repeated field 

1912 parts.append([]) 

1913 seen_fields.clear() 

1914 seen_fields.add(tok_value[0]) 

1915 parts[-1].append((tok_type, tok_value)) 

1916 

1917 return [untokenize_pattern(tokens) for tokens in parts] 

1918 

1919 

1920def match_skeleton(skeleton: str, options: Iterable[str], allow_different_fields: bool = False) -> str | None: 

1921 """ 

1922 Find the closest match for the given datetime skeleton among the options given. 

1923 

1924 This uses the rules outlined in the TR35 document. 

1925 

1926 >>> match_skeleton('yMMd', ('yMd', 'yMMMd')) 

1927 'yMd' 

1928 

1929 >>> match_skeleton('yMMd', ('jyMMd',), allow_different_fields=True) 

1930 'jyMMd' 

1931 

1932 >>> match_skeleton('yMMd', ('qyMMd',), allow_different_fields=False) 

1933 

1934 >>> match_skeleton('hmz', ('hmv',)) 

1935 'hmv' 

1936 

1937 :param skeleton: The skeleton to match 

1938 :type skeleton: str 

1939 :param options: An iterable of other skeletons to match against 

1940 :type options: Iterable[str] 

1941 :param allow_different_fields: Whether to allow a match that uses different fields 

1942 than the skeleton requested. 

1943 :type allow_different_fields: bool 

1944 

1945 :return: The closest skeleton match, or if no match was found, None. 

1946 :rtype: str|None 

1947 """ 

1948 

1949 # TODO: maybe implement pattern expansion? 

1950 

1951 # Based on the implementation in 

1952 # https://github.com/unicode-org/icu/blob/main/icu4j/main/core/src/main/java/com/ibm/icu/text/DateIntervalInfo.java 

1953 

1954 # Filter out falsy values and sort for stability; when `interval_formats` is passed in, there may be a None key. 

1955 options = sorted(option for option in options if option) 

1956 

1957 if 'z' in skeleton and not any('z' in option for option in options): 

1958 skeleton = skeleton.replace('z', 'v') 

1959 if 'k' in skeleton and not any('k' in option for option in options): 

1960 skeleton = skeleton.replace('k', 'H') 

1961 if 'K' in skeleton and not any('K' in option for option in options): 

1962 skeleton = skeleton.replace('K', 'h') 

1963 if 'a' in skeleton and not any('a' in option for option in options): 

1964 skeleton = skeleton.replace('a', '') 

1965 if 'b' in skeleton and not any('b' in option for option in options): 

1966 skeleton = skeleton.replace('b', '') 

1967 

1968 get_input_field_width = dict(t[1] for t in tokenize_pattern(skeleton) if t[0] == "field").get 

1969 best_skeleton = None 

1970 best_distance = None 

1971 for option in options: 

1972 get_opt_field_width = dict(t[1] for t in tokenize_pattern(option) if t[0] == "field").get 

1973 distance = 0 

1974 for field in PATTERN_CHARS: 

1975 input_width = get_input_field_width(field, 0) 

1976 opt_width = get_opt_field_width(field, 0) 

1977 if input_width == opt_width: 

1978 continue 

1979 if opt_width == 0 or input_width == 0: 

1980 if not allow_different_fields: # This one is not okay 

1981 option = None 

1982 break 

1983 distance += 0x1000 # Magic weight constant for "entirely different fields" 

1984 elif field == 'M' and ((input_width > 2 and opt_width <= 2) or (input_width <= 2 and opt_width > 2)): 

1985 distance += 0x100 # Magic weight for "text turns into a number" 

1986 else: 

1987 distance += abs(input_width - opt_width) 

1988 

1989 if not option: # We lost the option along the way (probably due to "allow_different_fields") 

1990 continue 

1991 

1992 if not best_skeleton or distance < best_distance: 

1993 best_skeleton = option 

1994 best_distance = distance 

1995 

1996 if distance == 0: # Found a perfect match! 

1997 break 

1998 

1999 return best_skeleton