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

726 statements  

1""" 

2babel.dates 

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

4 

5Locale dependent formatting and parsing of dates and times. 

6 

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

8following environment variables, in that order: 

9 

10 * ``LC_TIME``, 

11 * ``LC_ALL``, and 

12 * ``LANG`` 

13 

14:copyright: (c) 2013-2026 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 

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 

46 

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 

53 

54NO_INHERITANCE_MARKER = '\u2205\u2205\u2205' 

55 

56UTC = datetime.timezone.utc 

57LOCALTZ = localtime.LOCALTZ 

58 

59LC_TIME = default_locale('LC_TIME') 

60 

61 

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 

67 

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

69 return tz.localize(dt) 

70 

71 if dt.tzinfo is None: 

72 # convert naive to localized 

73 return dt.replace(tzinfo=tz) 

74 

75 # convert timezones 

76 return dt.astimezone(tz) 

77 

78 

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. 

84 

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

86 

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 

105 

106 

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

108 """ 

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

110 

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

120 

121 

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

123 """ 

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

125 

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

127 

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. 

130 

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

132 

133 >>> from datetime import date, datetime 

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

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

136 

137 UNIX timestamps are converted to datetimes. 

138 

139 >>> _get_datetime(1400000000) 

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

141 

142 Other values are passed through as-is. 

143 

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

145 >>> _get_datetime(x) is x 

146 True 

147 

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

149 :return: a datetime 

150 """ 

151 if instant is None: 

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

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

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

155 elif isinstance(instant, datetime.time): 

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

157 elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): # fmt: skip 

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

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

160 return instant 

161 

162 

163def _ensure_datetime_tzinfo( 

164 dt: datetime.datetime, 

165 tzinfo: datetime.tzinfo | None = None, 

166) -> datetime.datetime: 

167 """ 

168 Ensure the datetime passed has an attached tzinfo. 

169 

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

171 

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

173 

174 >>> from datetime import datetime 

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

176 'UTC' 

177 

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

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

180 14 

181 

182 :param datetime: Datetime to augment. 

183 :param tzinfo: optional tzinfo 

184 :return: datetime with tzinfo 

185 :rtype: datetime 

186 """ 

187 if dt.tzinfo is None: 

188 dt = dt.replace(tzinfo=UTC) 

189 if tzinfo is not None: 

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

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

192 dt = tzinfo.normalize(dt) 

193 return dt 

194 

195 

196def _get_time( 

197 time: datetime.time | datetime.datetime | None, 

198 tzinfo: datetime.tzinfo | None = None, 

199) -> datetime.time: 

200 """ 

201 Get a timezoned time from a given instant. 

202 

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

204 

205 :param time: time, datetime or None 

206 :rtype: time 

207 """ 

208 if time is None: 

209 time = datetime.datetime.now(UTC) 

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

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

212 

213 if time.tzinfo is None: 

214 time = time.replace(tzinfo=UTC) 

215 

216 if isinstance(time, datetime.datetime): 

217 if tzinfo is not None: 

218 time = time.astimezone(tzinfo) 

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

220 time = tzinfo.normalize(time) 

221 time = time.timetz() 

222 elif tzinfo is not None: 

223 time = time.replace(tzinfo=tzinfo) 

224 return time 

225 

226 

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

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

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

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

231 the functions of Babel that operate with dates. 

232 

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

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

235 

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

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

238 """ 

239 if zone is None: 

240 return LOCALTZ 

241 if not isinstance(zone, str): 

242 return zone 

243 

244 if pytz: 

245 try: 

246 return pytz.timezone(zone) 

247 except pytz.UnknownTimeZoneError as e: 

248 exc = e 

249 else: 

250 assert zoneinfo 

251 try: 

252 return zoneinfo.ZoneInfo(zone) 

253 except zoneinfo.ZoneInfoNotFoundError as e: 

254 exc = e 

255 

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

257 

258 

259def get_period_names( 

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

261 context: _Context = 'stand-alone', 

262 locale: Locale | str | None = None, 

263) -> LocaleDataDict: 

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

265 

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

267 'AM' 

268 

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

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

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

272 """ 

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

274 

275 

276def get_day_names( 

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

278 context: _Context = 'format', 

279 locale: Locale | str | None = None, 

280) -> LocaleDataDict: 

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

282 

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

284 'Tuesday' 

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

286 'Tu' 

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

288 'mar' 

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

290 'D' 

291 

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

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

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

295 """ 

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

297 

298 

299def get_month_names( 

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

301 context: _Context = 'format', 

302 locale: Locale | str | None = None, 

303) -> LocaleDataDict: 

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

305 

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

307 'January' 

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

309 'ene' 

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

311 'J' 

312 

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

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

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

316 """ 

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

318 

319 

320def get_quarter_names( 

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

322 context: _Context = 'format', 

323 locale: Locale | str | None = None, 

324) -> LocaleDataDict: 

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

326 

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

328 '1st quarter' 

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

330 'Q1' 

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

332 '1' 

333 

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

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

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

337 """ 

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

339 

340 

341def get_era_names( 

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

343 locale: Locale | str | None = None, 

344) -> LocaleDataDict: 

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

346 

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

348 'Anno Domini' 

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

350 'n. Chr.' 

351 

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

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

354 """ 

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

356 

357 

358def get_date_format( 

359 format: _PredefinedTimeFormat = 'medium', 

360 locale: Locale | str | None = None, 

361) -> DateTimePattern: 

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

363 format. 

364 

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

366 <DateTimePattern 'MMM d, y'> 

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

368 <DateTimePattern 'EEEE, d. MMMM y'> 

369 

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

371 "short" 

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

373 """ 

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

375 

376 

377def get_datetime_format( 

378 format: _PredefinedTimeFormat = 'medium', 

379 locale: Locale | str | None = None, 

380) -> DateTimePattern: 

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

382 specified format. 

383 

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

385 '{1}, {0}' 

386 

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

388 "short" 

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

390 """ 

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

392 if format not in patterns: 

393 format = None 

394 return patterns[format] 

395 

396 

397def get_time_format( 

398 format: _PredefinedTimeFormat = 'medium', 

399 locale: Locale | str | None = None, 

400) -> DateTimePattern: 

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

402 format. 

403 

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

405 <DateTimePattern 'h:mm:ss\\u202fa'> 

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

407 <DateTimePattern 'HH:mm:ss zzzz'> 

408 

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

410 "short" 

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

412 """ 

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

414 

415 

416def get_timezone_gmt( 

417 datetime: _Instant = None, 

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

419 locale: Locale | str | None = None, 

420 return_z: bool = False, 

421) -> str: 

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

423 as string indicating the offset from GMT. 

424 

425 >>> from datetime import datetime 

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

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

428 'GMT+00:00' 

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

430 'Z' 

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

432 '+00' 

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

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

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

436 'GMT-07:00' 

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

438 '-0700' 

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

440 '-07' 

441 

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

443 UTC string is used instead of GMT: 

444 

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

446 'UTC-07:00' 

447 

448 .. versionadded:: 0.9 

449 

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

451 time in UTC is used 

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

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

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

455 when local time offset is 0 

456 """ 

457 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime)) 

458 locale = Locale.parse(locale or LC_TIME) 

459 

460 offset = datetime.tzinfo.utcoffset(datetime) 

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

462 if return_z and seconds == 0: 

463 return 'Z' 

464 sign = '-' if seconds < 0 else '+' 

465 hours, seconds = divmod(abs(seconds), 3600) 

466 if seconds == 0 and width == 'iso8601_short': 

467 return '%s%02d' % (sign, hours) 

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

469 pattern = '%s%02d%02d' 

470 elif width == 'iso8601': 

471 pattern = '%s%02d:%02d' 

472 else: 

473 pattern = locale.zone_formats['gmt'] % '%s%02d:%02d' 

474 return pattern % (sign, hours, seconds // 60) 

475 

476 

477def get_timezone_location( 

478 dt_or_tzinfo: _DtOrTzinfo = None, 

479 locale: Locale | str | None = None, 

480 return_city: bool = False, 

481) -> str: 

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

483 

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

485 city associated with the time zone: 

486 

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

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

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

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

491 Canada (St. John’s) Time 

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

493 St. John’s 

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

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

496 'Mexiko (Mexiko-Stadt) (Ortszeit)' 

497 

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

499 timezone, just the localized country name is returned: 

500 

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

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

503 'Mitteleuropäische Zeit' 

504 

505 .. versionadded:: 0.9 

506 

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

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

509 UTC is assumed 

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

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

512 for the time zone 

513 :return: the localized timezone name using location format 

514 

515 """ 

516 locale = Locale.parse(locale or LC_TIME) 

517 

518 zone = _get_tz_name(dt_or_tzinfo) 

519 

520 # Get the canonical time-zone code 

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

522 

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

524 

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

526 # localized country name 

527 region_format = locale.zone_formats['region'] 

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

529 if territory not in locale.territories: 

530 territory = 'ZZ' # invalid/unknown 

531 territory_name = locale.territories[territory] 

532 if ( 

533 not return_city 

534 and territory 

535 and len(get_global('territory_zones').get(territory, [])) == 1 

536 ): 

537 return region_format % territory_name 

538 

539 # Otherwise, include the city in the output 

540 fallback_format = locale.zone_formats['fallback'] 

541 if 'city' in info: 

542 city_name = info['city'] 

543 else: 

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

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

546 if 'city' in metazone_info: 

547 city_name = metazone_info['city'] 

548 elif '/' in zone: 

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

550 else: 

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

552 

553 if return_city: 

554 return city_name 

555 return region_format % ( 

556 fallback_format 

557 % { 

558 '0': city_name, 

559 '1': territory_name, 

560 } 

561 ) 

562 

563 

564def get_timezone_name( 

565 dt_or_tzinfo: _DtOrTzinfo = None, 

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

567 uncommon: bool = False, 

568 locale: Locale | str | None = None, 

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

570 return_zone: bool = False, 

571) -> str: 

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

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

574 

575 >>> from datetime import time 

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

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

578 'Pacific Standard Time' 

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

580 'America/Los_Angeles' 

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

582 'PST' 

583 

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

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

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

587 time of events that recur across DST changes: 

588 

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

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

591 'Pacific Time' 

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

593 'PT' 

594 

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

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

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

598 

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

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

601 'Mitteleuropäische Zeit' 

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

603 'Horário da Europa Central' 

604 

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

606 included in the representation: 

607 

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

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

610 'Neufundland-Zeit' 

611 

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

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

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

615 format. 

616 

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

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

619 

620 .. versionadded:: 0.9 

621 

622 .. versionchanged:: 1.0 

623 Added `zone_variant` support. 

624 

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

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

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

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

629 current date in UTC is assumed 

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

631 :param uncommon: deprecated and ignored 

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

633 variation is defined from the datetime object 

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

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

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

637 ``'standard'``. 

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

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

640 returns long time zone ID 

641 """ 

642 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) 

643 locale = Locale.parse(locale or LC_TIME) 

644 

645 zone = _get_tz_name(dt_or_tzinfo) 

646 

647 if zone_variant is None: 

648 if dt is None: 

649 zone_variant = 'generic' 

650 else: 

651 dst = tzinfo.dst(dt) 

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

653 else: 

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

655 raise ValueError('Invalid zone variation') 

656 

657 # Get the canonical time-zone code 

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

659 if return_zone: 

660 return zone 

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

662 # Try explicitly translated zone names first 

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

664 value = info[width][zone_variant] 

665 if value != NO_INHERITANCE_MARKER: 

666 return value 

667 

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

669 if metazone: 

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

671 if width in metazone_info: 

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

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

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

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

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

677 if name and name != NO_INHERITANCE_MARKER: 

678 return name 

679 

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

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

682 if dt is not None: 

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

684 

685 return get_timezone_location(dt_or_tzinfo, locale=locale) 

686 

687 

688def format_date( 

689 date: datetime.date | None = None, 

690 format: _PredefinedTimeFormat | str = 'medium', 

691 locale: Locale | str | None = None, 

692) -> str: 

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

694 

695 >>> from datetime import date 

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

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

698 'Apr 1, 2007' 

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

700 'Sonntag, 1. April 2007' 

701 

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

703 custom date pattern: 

704 

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

706 "Sun, Apr 1, '07" 

707 

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

709 date is used 

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

711 date/time pattern 

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

713 """ 

714 if date is None: 

715 date = datetime.date.today() 

716 elif isinstance(date, datetime.datetime): 

717 date = date.date() 

718 

719 locale = Locale.parse(locale or LC_TIME) 

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

721 format = get_date_format(format, locale=locale) 

722 pattern = parse_pattern(format) 

723 return pattern.apply(date, locale) 

724 

725 

726def format_datetime( 

727 datetime: _Instant = None, 

728 format: _PredefinedTimeFormat | str = 'medium', 

729 tzinfo: datetime.tzinfo | None = None, 

730 locale: Locale | str | None = None, 

731) -> str: 

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

733 

734 >>> from datetime import datetime 

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

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

737 'Apr 1, 2007, 3:30:00\u202fPM' 

738 

739 For any pattern requiring the display of the timezone: 

740 

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

742 ... locale='fr_FR') 

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

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

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

746 '2007.04.01 AD at 11:30:00 EDT' 

747 

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

749 time is used 

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

751 date/time pattern 

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

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

754 """ 

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

756 

757 locale = Locale.parse(locale or LC_TIME) 

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

759 return ( 

760 get_datetime_format(format, locale=locale) 

761 .replace("'", "") 

762 .replace('{0}', format_time(datetime, format, tzinfo=None, locale=locale)) 

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

764 ) 

765 else: 

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

767 

768 

769def format_time( 

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

771 format: _PredefinedTimeFormat | str = 'medium', 

772 tzinfo: datetime.tzinfo | None = None, 

773 locale: Locale | str | None = None, 

774) -> str: 

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

776 

777 >>> from datetime import datetime, time 

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

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

780 '3:30:00\u202fPM' 

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

782 '15:30' 

783 

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

785 custom time pattern: 

786 

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

788 "03 o'clock PM" 

789 

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

791 timezone has to be specified explicitly: 

792 

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

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

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

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

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

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

799 ... locale='en') 

800 "09 o'clock AM, Eastern Daylight Time" 

801 

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

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

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

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

806 it is assumed to be in UTC. 

807 

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

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

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

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

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

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

814 

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

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

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

818 '15:30:00 heure normale d\u2019Europe centrale' 

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

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

821 '3:30:00\u202fPM Eastern Standard Time' 

822 

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

824 time in UTC is used 

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

826 date/time pattern 

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

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

829 """ 

830 

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

832 # in the pattern 

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

834 

835 time = _get_time(time, tzinfo) 

836 

837 locale = Locale.parse(locale or LC_TIME) 

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

839 format = get_time_format(format, locale=locale) 

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

841 

842 

843def format_skeleton( 

844 skeleton: str, 

845 datetime: _Instant = None, 

846 tzinfo: datetime.tzinfo | None = None, 

847 fuzzy: bool = True, 

848 locale: Locale | str | None = None, 

849) -> str: 

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

851 

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

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

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

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

856 

857 >>> from datetime import datetime 

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

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

860 'dim. 1 avr.' 

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

862 'Sun, Apr 1' 

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

864 '1.4.2007' 

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

866 Traceback (most recent call last): 

867 ... 

868 KeyError: yMMd 

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

870 Traceback (most recent call last): 

871 ... 

872 KeyError: None 

873 

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

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

876 

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

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

879 time in UTC is used 

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

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

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

883 is thrown. 

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

885 """ 

886 locale = Locale.parse(locale or LC_TIME) 

887 if fuzzy and skeleton not in locale.datetime_skeletons: 

888 skeleton = match_skeleton(skeleton, locale.datetime_skeletons) 

889 format = locale.datetime_skeletons[skeleton] 

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

891 

892 

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

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

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

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

897 ('day', 3600 * 24), 

898 ('hour', 3600), 

899 ('minute', 60), 

900 ('second', 1), 

901) 

902 

903 

904def format_timedelta( 

905 delta: datetime.timedelta | int, 

906 granularity: Literal[ 

907 'year', 

908 'month', 

909 'week', 

910 'day', 

911 'hour', 

912 'minute', 

913 'second', 

914 ] = 'second', 

915 threshold: float = 0.85, 

916 add_direction: bool = False, 

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

918 locale: Locale | str | None = None, 

919) -> str: 

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

921 

922 >>> from datetime import timedelta 

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

924 '3 months' 

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

926 '1 segundo' 

927 

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

929 presented, which defaults to a second. 

930 

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

932 '1 day' 

933 

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

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

936 means the presentation will switch later. For example: 

937 

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

939 '1 day' 

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

941 '23 hours' 

942 

943 In addition directional information can be provided that informs 

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

945 

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

947 'in 1 hour' 

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

949 '1 hour ago' 

950 

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

952 

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

954 '3 hr' 

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

956 '3h' 

957 

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

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

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

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

962 "hour", "minute" or "second" 

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

964 switches to the next higher unit 

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

966 include directional information. For instance a 

967 positive timedelta will include the information about 

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

969 about the value being in the past. 

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

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

972 maintain compatibility) 

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

974 """ 

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

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

977 if format == 'medium': 

978 warnings.warn( 

979 '"medium" value for format param of format_timedelta is deprecated. Use "long" instead', 

980 category=DeprecationWarning, 

981 stacklevel=2, 

982 ) 

983 format = 'long' 

984 if isinstance(delta, datetime.timedelta): 

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

986 else: 

987 seconds = delta 

988 locale = Locale.parse(locale or LC_TIME) 

989 date_fields = locale._data["date_fields"] 

990 unit_patterns = locale._data["unit_patterns"] 

991 

992 def _iter_patterns(a_unit): 

993 if add_direction: 

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

995 # before falling back to the default. 

996 unit_rel_patterns = date_fields.get(f"{a_unit}-{format}") or date_fields.get(a_unit) or {} 

997 if seconds >= 0: 

998 yield unit_rel_patterns['future'] 

999 else: 

1000 yield unit_rel_patterns['past'] 

1001 a_unit = f"duration-{a_unit}" 

1002 yield unit_patterns.get(a_unit, {}).get(format) # resolves aliases 

1003 

1004 for unit, secs_per_unit in TIMEDELTA_UNITS: 

1005 value = abs(seconds) / secs_per_unit 

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

1007 if unit == granularity and value > 0: 

1008 value = max(1, value) 

1009 value = int(round(value)) 

1010 plural_form = locale.plural_form(value) 

1011 pattern = None 

1012 for patterns in _iter_patterns(unit): 

1013 if patterns is not None: 

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

1015 if pattern: 

1016 break 

1017 # This really should not happen 

1018 if pattern is None: 

1019 return '' 

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

1021 

1022 return '' 

1023 

1024 

1025def _format_fallback_interval( 

1026 start: _Instant, 

1027 end: _Instant, 

1028 skeleton: str | None, 

1029 tzinfo: datetime.tzinfo | None, 

1030 locale: Locale, 

1031) -> str: 

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

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

1034 elif all( 

1035 # Both are just dates 

1036 (isinstance(d, datetime.date) and not isinstance(d, datetime.datetime)) 

1037 for d in (start, end) 

1038 ): 

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

1040 elif all( 

1041 # Both are times 

1042 (isinstance(d, datetime.time) and not isinstance(d, datetime.date)) 

1043 for d in (start, end) 

1044 ): 

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

1046 else: 

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

1048 

1049 formatted_start = format(start) 

1050 formatted_end = format(end) 

1051 

1052 if formatted_start == formatted_end: 

1053 return format(start) 

1054 

1055 return ( 

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

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

1058 .replace("{1}", formatted_end) 

1059 ) 

1060 

1061 

1062def format_interval( 

1063 start: _Instant, 

1064 end: _Instant, 

1065 skeleton: str | None = None, 

1066 tzinfo: datetime.tzinfo | None = None, 

1067 fuzzy: bool = True, 

1068 locale: Locale | str | None = None, 

1069) -> str: 

1070 """ 

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

1072 

1073 >>> from datetime import date, time 

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

1075 '15.–17.1.2016' 

1076 

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

1078 '12:12–16:16' 

1079 

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

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

1082 

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

1084 '16:18–16:24' 

1085 

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

1087 

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

1089 '16:18' 

1090 

1091 Unknown skeletons fall back to "default" formatting. 

1092 

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

1094 '2015/01/01~2017/01/01' 

1095 

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

1097 '16:18:00~16:24:00' 

1098 

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

1100 '15.01.2016\\u2009–\\u200917.01.2016' 

1101 

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

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

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

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

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

1107 close enough to it. 

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

1109 :return: Formatted interval 

1110 """ 

1111 locale = Locale.parse(locale or LC_TIME) 

1112 

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

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

1115 

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

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

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

1119 

1120 interval_formats = locale.interval_formats 

1121 

1122 if skeleton not in interval_formats or not skeleton: 

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

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

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

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

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

1128 if skeleton and fuzzy: 

1129 skeleton = match_skeleton(skeleton, interval_formats) 

1130 else: 

1131 skeleton = None 

1132 if not skeleton: # Still no match whatsoever? 

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

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

1135 

1136 skel_formats = interval_formats[skeleton] 

1137 

1138 if start == end: 

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

1140 

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

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

1143 

1144 start_fmt = DateTimeFormat(start, locale=locale) 

1145 end_fmt = DateTimeFormat(end, locale=locale) 

1146 

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

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

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

1150 # > single date using availableFormats, and return. 

1151 

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

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

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

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

1156 return "".join( 

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

1158 for pattern, instant in zip(skel_formats[field], (start, end)) 

1159 ) 

1160 

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

1162 

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

1164 

1165 

1166def get_period_id( 

1167 time: _Instant, 

1168 tzinfo: datetime.tzinfo | None = None, 

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

1170 locale: Locale | str | None = None, 

1171) -> str: 

1172 """ 

1173 Get the day period ID for a given time. 

1174 

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

1176 

1177 >>> from datetime import time 

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

1179 'Morgen' 

1180 

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

1182 'midnight' 

1183 

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

1185 'morning1' 

1186 

1187 :param time: The time to inspect. 

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

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

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

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

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

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

1194 """ 

1195 time = _get_time(time, tzinfo) 

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

1197 locale = Locale.parse(locale or LC_TIME) 

1198 

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

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

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

1202 

1203 for rule_id, rules in rulesets: 

1204 for rule in rules: 

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

1206 return rule_id 

1207 

1208 for rule_id, rules in rulesets: 

1209 for rule in rules: 

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

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

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

1213 return rule_id 

1214 else: 

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

1216 if ( 

1217 rule["from"] <= seconds_past_midnight < 86400 

1218 or 0 <= seconds_past_midnight < rule["before"] 

1219 ): 

1220 return rule_id 

1221 

1222 start_ok = end_ok = False 

1223 

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

1225 start_ok = True 

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

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

1228 # excuse the lack of test coverage. 

1229 end_ok = True 

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

1231 end_ok = True 

1232 if "after" in rule: 

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

1234 

1235 if start_ok and end_ok: 

1236 return rule_id 

1237 

1238 if seconds_past_midnight < 43200: 

1239 return "am" 

1240 else: 

1241 return "pm" 

1242 

1243 

1244class ParseError(ValueError): 

1245 pass 

1246 

1247 

1248def parse_date( 

1249 string: str, 

1250 locale: Locale | str | None = None, 

1251 format: _PredefinedTimeFormat | str = 'medium', 

1252) -> datetime.date: 

1253 """Parse a date from a string. 

1254 

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

1256 

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

1258 datetime.date(2004, 4, 1) 

1259 

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

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

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

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

1264 the string. 

1265 

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

1267 datetime.date(2004, 4, 1) 

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

1269 datetime.date(2004, 4, 1) 

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

1271 datetime.date(2004, 4, 1) 

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

1273 datetime.date(2004, 4, 1) 

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

1275 datetime.date(2004, 4, 1) 

1276 

1277 :param string: the string containing the date 

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

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

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

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

1282 (see ``get_time_format``) 

1283 """ 

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

1285 if not numbers: 

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

1287 

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

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

1290 # extended YYYY-MM-DD or basic YYYYMMDD 

1291 iso_alike = re.match( 

1292 r'^(\d{4})-?([01]\d)-?([0-3]\d)$', 

1293 string, 

1294 flags=re.ASCII, # allow only ASCII digits 

1295 ) 

1296 if iso_alike and use_predefined_format: 

1297 try: 

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

1299 except ValueError: 

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

1301 

1302 if use_predefined_format: 

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

1304 else: 

1305 fmt = parse_pattern(format) 

1306 format_str = fmt.pattern.lower() 

1307 year_idx = format_str.index('y') 

1308 month_idx = format_str.find('m') 

1309 if month_idx < 0: 

1310 month_idx = format_str.index('l') 

1311 day_idx = format_str.index('d') 

1312 

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

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

1315 

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

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

1318 

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

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

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

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

1323 if month > 12: 

1324 month, day = day, month 

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

1326 

1327 

1328def parse_time( 

1329 string: str, 

1330 locale: Locale | str | None = None, 

1331 format: _PredefinedTimeFormat | str = 'medium', 

1332) -> datetime.time: 

1333 """Parse a time from a string. 

1334 

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

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

1337 

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

1339 the time instead. 

1340 

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

1342 datetime.time(15, 30) 

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

1344 datetime.time(15, 30) 

1345 

1346 :param string: the string containing the time 

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

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

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

1350 (see ``get_time_format``) 

1351 :return: the parsed time 

1352 :rtype: `time` 

1353 """ 

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

1355 if not numbers: 

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

1357 

1358 # TODO: try ISO format first? 

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

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

1361 else: 

1362 fmt = parse_pattern(format) 

1363 format_str = fmt.pattern.lower() 

1364 hour_idx = format_str.find('h') 

1365 if hour_idx < 0: 

1366 hour_idx = format_str.index('k') 

1367 min_idx = format_str.index('m') 

1368 # format might not contain seconds 

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

1370 sec_idx = math.inf 

1371 

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

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

1374 

1375 # TODO: support time zones 

1376 

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

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

1379 hour_offset = 0 

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

1381 hour_offset = 12 

1382 

1383 # Parse up to three numbers from the string. 

1384 minute = second = 0 

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

1386 if len(numbers) > 1: 

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

1388 if len(numbers) > 2: 

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

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

1391 

1392 

1393class DateTimePattern: 

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

1395 self.pattern = pattern 

1396 self.format = format 

1397 

1398 def __repr__(self) -> str: 

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

1400 

1401 def __str__(self) -> str: 

1402 pat = self.pattern 

1403 return pat 

1404 

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

1406 if not isinstance(other, DateTimeFormat): 

1407 return NotImplemented 

1408 return self.format % other 

1409 

1410 def apply( 

1411 self, 

1412 datetime: datetime.date | datetime.time, 

1413 locale: Locale | str | None, 

1414 reference_date: datetime.date | None = None, 

1415 ) -> str: 

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

1417 

1418 

1419class DateTimeFormat: 

1420 def __init__( 

1421 self, 

1422 value: datetime.date | datetime.time, 

1423 locale: Locale | str, 

1424 reference_date: datetime.date | None = None, 

1425 ) -> None: 

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

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

1428 value = value.replace(tzinfo=UTC) 

1429 self.value = value 

1430 self.locale = Locale.parse(locale) 

1431 self.reference_date = reference_date 

1432 

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

1434 char = name[0] 

1435 num = len(name) 

1436 if char == 'G': 

1437 return self.format_era(char, num) 

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

1439 return self.format_year(char, num) 

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

1441 return self.format_quarter(char, num) 

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

1443 return self.format_month(char, num) 

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

1445 return self.format_week(char, num) 

1446 elif char == 'd': 

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

1448 elif char == 'D': 

1449 return self.format_day_of_year(num) 

1450 elif char == 'F': 

1451 return self.format_day_of_week_in_month() 

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

1453 return self.format_weekday(char, num) 

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

1455 return self.format_period(char, num) 

1456 elif char == 'h': 

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

1458 return self.format(12, num) 

1459 else: 

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

1461 elif char == 'H': 

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

1463 elif char == 'K': 

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

1465 elif char == 'k': 

1466 if self.value.hour == 0: 

1467 return self.format(24, num) 

1468 else: 

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

1470 elif char == 'm': 

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

1472 elif char == 's': 

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

1474 elif char == 'S': 

1475 return self.format_frac_seconds(num) 

1476 elif char == 'A': 

1477 return self.format_milliseconds_in_day(num) 

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

1479 return self.format_timezone(char, num) 

1480 else: 

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

1482 

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

1484 char = str(char)[0] 

1485 if char == 'y': 

1486 return self.value.year 

1487 elif char == 'M': 

1488 return self.value.month 

1489 elif char == 'd': 

1490 return self.value.day 

1491 elif char == 'H': 

1492 return self.value.hour 

1493 elif char == 'h': 

1494 return self.value.hour % 12 or 12 

1495 elif char == 'm': 

1496 return self.value.minute 

1497 elif char == 'a': 

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

1499 else: 

1500 raise NotImplementedError( 

1501 f"Not implemented: extracting {char!r} from {self.value!r}", 

1502 ) 

1503 

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

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

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

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

1508 

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

1510 value = self.value.year 

1511 if char.isupper(): 

1512 month = self.value.month 

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

1514 value -= 1 

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

1516 value += 1 

1517 year = self.format(value, num) 

1518 if num == 2: 

1519 year = year[-2:] 

1520 return year 

1521 

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

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

1524 if num <= 2: 

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

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

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

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

1529 

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

1531 if num <= 2: 

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

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

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

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

1536 

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

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

1539 week = self.get_week_of_year() 

1540 return self.format(week, num) 

1541 else: # week of month 

1542 week = self.get_week_of_month() 

1543 return str(week) 

1544 

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

1546 """ 

1547 Return weekday from parsed datetime according to format pattern. 

1548 

1549 >>> from datetime import date 

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

1551 >>> format.format_weekday() 

1552 'Sunday' 

1553 

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

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

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

1557 'Sun' 

1558 

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

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

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

1562 '01' 

1563 

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

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

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

1567 '1' 

1568 

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

1570 :param num: count of format character 

1571 

1572 """ 

1573 if num < 3: 

1574 if char.islower(): 

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

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

1577 num = 3 

1578 weekday = self.value.weekday() 

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

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

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

1582 

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

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

1585 

1586 def format_day_of_week_in_month(self) -> str: 

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

1588 

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

1590 """ 

1591 Return period from parsed datetime according to format pattern. 

1592 

1593 >>> from datetime import datetime, time 

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

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

1596 'ip.' 

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

1598 'iltap.' 

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

1600 'iltapäivä' 

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

1602 'iltapäivällä' 

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

1604 'ip.' 

1605 

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

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

1608 '上午' 

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

1610 '清晨' 

1611 

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

1613 :param num: count of format character 

1614 

1615 """ 

1616 widths = [ 

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

1618 'wide', 

1619 'narrow', 

1620 'abbreviated', 

1621 ] 

1622 if char == 'a': 

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

1624 context = 'format' 

1625 else: 

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

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

1628 for width in widths: 

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

1630 if period in period_names: 

1631 return period_names[period] 

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

1633 

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

1635 """ Return fractional seconds. 

1636 

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

1638 of digits passed in. 

1639 """ 

1640 value = self.value.microsecond / 1000000 

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

1642 

1643 def format_milliseconds_in_day(self, num): 

1644 msecs = ( 

1645 self.value.microsecond // 1000 

1646 + self.value.second * 1000 

1647 + self.value.minute * 60000 

1648 + self.value.hour * 3600000 

1649 ) 

1650 return self.format(msecs, num) 

1651 

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

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

1654 

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

1656 # reference date which is important to distinguish between timezone 

1657 # variants (summer/standard time) 

1658 value = self.value 

1659 if self.reference_date: 

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

1661 

1662 if char == 'z': 

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

1664 elif char == 'Z': 

1665 if num == 5: 

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

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

1668 elif char == 'O': 

1669 if num == 4: 

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

1671 # TODO: To add support for O:1 

1672 elif char == 'v': 

1673 return get_timezone_name(value.tzinfo, width, locale=self.locale) 

1674 elif char == 'V': 

1675 if num == 1: 

1676 return get_timezone_name(value.tzinfo, width, locale=self.locale) 

1677 elif num == 2: 

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

1679 elif num == 3: 

1680 return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True) # fmt: skip 

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

1682 elif char in 'Xx': 

1683 return_z = char == 'X' 

1684 if num == 1: 

1685 width = 'iso8601_short' 

1686 elif num in (2, 4): 

1687 width = 'short' 

1688 elif num in (3, 5): 

1689 width = 'iso8601' 

1690 return get_timezone_gmt(value, width=width, locale=self.locale, return_z=return_z) # fmt: skip 

1691 

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

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

1694 

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

1696 if date is None: 

1697 date = self.value 

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

1699 

1700 def get_week_of_year(self) -> int: 

1701 """Return the week of the year.""" 

1702 day_of_year = self.get_day_of_year(self.value) 

1703 week = self.get_week_number(day_of_year) 

1704 if week == 0: 

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

1706 week = self.get_week_number(self.get_day_of_year(date), date.weekday()) 

1707 elif week > 52: 

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

1709 if ( 

1710 self.get_week_number(1, weekday) == 1 

1711 and 32 - (weekday - self.locale.first_week_day) % 7 <= self.value.day 

1712 ): 

1713 week = 1 

1714 return week 

1715 

1716 def get_week_of_month(self) -> int: 

1717 """Return the week of the month.""" 

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

1719 

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

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

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

1723 

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

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

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

1727 

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

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

1730 1 

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

1732 2 

1733 

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

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

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

1737 current date is assumed 

1738 """ 

1739 if day_of_week is None: 

1740 day_of_week = self.value.weekday() 

1741 first_day = (day_of_week - self.locale.first_week_day - day_of_period + 1) % 7 

1742 if first_day < 0: 

1743 first_day += 7 

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

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

1746 week_number += 1 

1747 return week_number 

1748 

1749 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1764} # fmt: skip 

1765 

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

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

1768#: in order of decreasing magnitude. 

1769PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" 

1770 

1771 

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

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

1774 

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

1776 '%(MMMM)s%(d)s' 

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

1778 '%(MMM)s %(d)s, %(yyyy)s' 

1779 

1780 Pattern can contain literal strings in single quotes: 

1781 

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

1783 '%(H)s:%(mm)s Uhr %(z)s' 

1784 

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

1786 characters: 

1787 

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

1789 "%(hh)s o'clock" 

1790 

1791 :param pattern: the formatting pattern to parse 

1792 """ 

1793 if isinstance(pattern, DateTimePattern): 

1794 return pattern 

1795 return _cached_parse_pattern(pattern) 

1796 

1797 

1798@lru_cache(maxsize=1024) 

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

1800 result = [] 

1801 

1802 for tok_type, tok_value in tokenize_pattern(pattern): 

1803 if tok_type == "chars": 

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

1805 elif tok_type == "field": 

1806 fieldchar, fieldnum = tok_value 

1807 limit = PATTERN_CHARS[fieldchar] 

1808 if limit and fieldnum not in limit: 

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

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

1811 else: 

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

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

1814 

1815 

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

1817 """ 

1818 Tokenize date format patterns. 

1819 

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

1821 

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

1823 

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

1825 

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

1827 

1828 :param pattern: Pattern string 

1829 """ 

1830 result = [] 

1831 quotebuf = None 

1832 charbuf = [] 

1833 fieldchar = [''] 

1834 fieldnum = [0] 

1835 

1836 def append_chars(): 

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

1838 del charbuf[:] 

1839 

1840 def append_field(): 

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

1842 fieldchar[0] = '' 

1843 fieldnum[0] = 0 

1844 

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

1846 if quotebuf is None: 

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

1848 if fieldchar[0]: 

1849 append_field() 

1850 elif charbuf: 

1851 append_chars() 

1852 quotebuf = [] 

1853 elif char in PATTERN_CHARS: 

1854 if charbuf: 

1855 append_chars() 

1856 if char == fieldchar[0]: 

1857 fieldnum[0] += 1 

1858 else: 

1859 if fieldchar[0]: 

1860 append_field() 

1861 fieldchar[0] = char 

1862 fieldnum[0] = 1 

1863 else: 

1864 if fieldchar[0]: 

1865 append_field() 

1866 charbuf.append(char) 

1867 

1868 elif quotebuf is not None: 

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

1870 charbuf.extend(quotebuf) 

1871 quotebuf = None 

1872 else: # inside quote 

1873 quotebuf.append(char) 

1874 

1875 if fieldchar[0]: 

1876 append_field() 

1877 elif charbuf: 

1878 append_chars() 

1879 

1880 return result 

1881 

1882 

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

1884 """ 

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

1886 

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

1888 """ 

1889 output = [] 

1890 for tok_type, tok_value in tokens: 

1891 if tok_type == "field": 

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

1893 elif tok_type == "chars": 

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

1895 output.append(tok_value) 

1896 else: 

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

1898 return "".join(output) 

1899 

1900 

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

1902 """ 

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

1904 

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

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

1907 

1908 >>> split_interval_pattern('E d.M. – E d.M.') 

1909 ['E d.M. – ', 'E d.M.'] 

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

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

1912 >>> split_interval_pattern('E, MMM d – E') 

1913 ['E, MMM d – ', 'E'] 

1914 >>> split_interval_pattern("MMM d") 

1915 ['MMM d'] 

1916 >>> split_interval_pattern("y G") 

1917 ['y G'] 

1918 >>> split_interval_pattern('MMM d – d') 

1919 ['MMM d – ', 'd'] 

1920 

1921 :param pattern: Interval pattern string 

1922 :return: list of "subpatterns" 

1923 """ 

1924 

1925 seen_fields = set() 

1926 parts = [[]] 

1927 

1928 for tok_type, tok_value in tokenize_pattern(pattern): 

1929 if tok_type == "field": 

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

1931 parts.append([]) 

1932 seen_fields.clear() 

1933 seen_fields.add(tok_value[0]) 

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

1935 

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

1937 

1938 

1939def match_skeleton( 

1940 skeleton: str, 

1941 options: Iterable[str], 

1942 allow_different_fields: bool = False, 

1943) -> str | None: 

1944 """ 

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

1946 

1947 This uses the rules outlined in the TR35 document. 

1948 

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

1950 'yMd' 

1951 

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

1953 'jyMMd' 

1954 

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

1956 

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

1958 'hmv' 

1959 

1960 :param skeleton: The skeleton to match 

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

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

1963 than the skeleton requested. 

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

1965 :rtype: str|None 

1966 """ 

1967 

1968 # TODO: maybe implement pattern expansion? 

1969 

1970 # Based on the implementation in 

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

1972 

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

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

1975 

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

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

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

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

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

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

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

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

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

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

1986 

1987 get_input_field_width = dict(t[1] for t in tokenize_pattern(skeleton) if t[0] == "field").get # fmt: skip 

1988 best_skeleton = None 

1989 best_distance = None 

1990 for option in options: 

1991 get_opt_field_width = dict(t[1] for t in tokenize_pattern(option) if t[0] == "field").get # fmt: skip 

1992 distance = 0 

1993 for field in PATTERN_CHARS: 

1994 input_width = get_input_field_width(field, 0) 

1995 opt_width = get_opt_field_width(field, 0) 

1996 if input_width == opt_width: 

1997 continue 

1998 if opt_width == 0 or input_width == 0: 

1999 if not allow_different_fields: # This one is not okay 

2000 option = None 

2001 break 

2002 # Magic weight constant for "entirely different fields" 

2003 distance += 0x1000 

2004 elif field == 'M' and ( 

2005 (input_width > 2 and opt_width <= 2) or (input_width <= 2 and opt_width > 2) 

2006 ): 

2007 # Magic weight constant for "text turns into a number" 

2008 distance += 0x100 

2009 else: 

2010 distance += abs(input_width - opt_width) 

2011 

2012 if not option: 

2013 # We lost the option along the way (probably due to "allow_different_fields") 

2014 continue 

2015 

2016 if not best_skeleton or distance < best_distance: 

2017 best_skeleton = option 

2018 best_distance = distance 

2019 

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

2021 break 

2022 

2023 return best_skeleton