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

733 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 '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 'Tuesday' 

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

282 'Tu' 

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

284 'mar' 

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

286 '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 'January' 

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

305 'ene' 

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

307 '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 '1st quarter' 

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

326 'Q1' 

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

328 '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 'Anno Domini' 

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

346 '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 'MMM d, y'> 

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

364 <DateTimePattern '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 '{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 'h:mm:ss\\u202fa'> 

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

403 <DateTimePattern '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 '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 '+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 'GMT-07:00' 

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

434 '-0700' 

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

436 '-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 '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 '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 'Mitteleuropäische 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 '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 '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 'Pacific Time' 

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

581 '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 'Mitteleuropäische Zeit' 

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

591 'Horário 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 '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 value = info[width][zone_variant] 

653 if value != NO_INHERITANCE_MARKER: 

654 return value 

655 

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

657 if metazone: 

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

659 if width in metazone_info: 

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

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

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

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

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

665 if name and name != NO_INHERITANCE_MARKER: 

666 return name 

667 

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

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

670 if dt is not None: 

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

672 

673 return get_timezone_location(dt_or_tzinfo, locale=locale) 

674 

675 

676def format_date( 

677 date: datetime.date | None = None, 

678 format: _PredefinedTimeFormat | str = 'medium', 

679 locale: Locale | str | None = None, 

680) -> str: 

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

682 

683 >>> from datetime import date 

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

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

686 'Apr 1, 2007' 

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

688 'Sonntag, 1. April 2007' 

689 

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

691 custom date pattern: 

692 

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

694 "Sun, Apr 1, '07" 

695 

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

697 date is used 

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

699 date/time pattern 

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

701 """ 

702 if date is None: 

703 date = datetime.date.today() 

704 elif isinstance(date, datetime.datetime): 

705 date = date.date() 

706 

707 locale = Locale.parse(locale or LC_TIME) 

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

709 format = get_date_format(format, locale=locale) 

710 pattern = parse_pattern(format) 

711 return pattern.apply(date, locale) 

712 

713 

714def format_datetime( 

715 datetime: _Instant = None, 

716 format: _PredefinedTimeFormat | str = 'medium', 

717 tzinfo: datetime.tzinfo | None = None, 

718 locale: Locale | str | None = None, 

719) -> str: 

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

721 

722 >>> from datetime import datetime 

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

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

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

726 

727 For any pattern requiring the display of the timezone: 

728 

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

730 ... locale='fr_FR') 

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

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

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

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

735 

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

737 time is used 

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

739 date/time pattern 

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

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

742 """ 

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

744 

745 locale = Locale.parse(locale or LC_TIME) 

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

747 return get_datetime_format(format, locale=locale) \ 

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

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

750 locale=locale)) \ 

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

752 else: 

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

754 

755 

756def format_time( 

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

758 format: _PredefinedTimeFormat | str = 'medium', 

759 tzinfo: datetime.tzinfo | None = None, 

760 locale: Locale | str | None = None, 

761) -> str: 

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

763 

764 >>> from datetime import datetime, time 

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

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

767 '3:30:00\u202fPM' 

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

769 '15:30' 

770 

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

772 custom time pattern: 

773 

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

775 "03 o'clock PM" 

776 

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

778 timezone has to be specified explicitly: 

779 

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

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

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

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

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

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

786 ... locale='en') 

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

788 

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

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

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

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

793 it is assumed to be in UTC. 

794 

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

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

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

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

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

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

801 

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

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

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

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

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

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

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

809 

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

811 time in UTC is used 

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

813 date/time pattern 

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

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

816 """ 

817 

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

819 # in the pattern 

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

821 

822 time = _get_time(time, tzinfo) 

823 

824 locale = Locale.parse(locale or LC_TIME) 

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

826 format = get_time_format(format, locale=locale) 

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

828 

829 

830def format_skeleton( 

831 skeleton: str, 

832 datetime: _Instant = None, 

833 tzinfo: datetime.tzinfo | None = None, 

834 fuzzy: bool = True, 

835 locale: Locale | str | None = None, 

836) -> str: 

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

838 

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

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

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

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

843 

844 >>> from datetime import datetime 

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

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

847 'dim. 1 avr.' 

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

849 'Sun, Apr 1' 

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

851 '1.4.2007' 

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

853 Traceback (most recent call last): 

854 ... 

855 KeyError: yMMd 

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

857 Traceback (most recent call last): 

858 ... 

859 KeyError: None 

860 

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

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

863 

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

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

866 time in UTC is used 

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

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

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

870 is thrown. 

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

872 """ 

873 locale = Locale.parse(locale or LC_TIME) 

874 if fuzzy and skeleton not in locale.datetime_skeletons: 

875 skeleton = match_skeleton(skeleton, locale.datetime_skeletons) 

876 format = locale.datetime_skeletons[skeleton] 

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

878 

879 

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

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

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

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

884 ('day', 3600 * 24), 

885 ('hour', 3600), 

886 ('minute', 60), 

887 ('second', 1), 

888) 

889 

890 

891def format_timedelta( 

892 delta: datetime.timedelta | int, 

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

894 threshold: float = .85, 

895 add_direction: bool = False, 

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

897 locale: Locale | str | None = None, 

898) -> str: 

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

900 

901 >>> from datetime import timedelta 

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

903 '3 months' 

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

905 '1 segundo' 

906 

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

908 presented, which defaults to a second. 

909 

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

911 '1 day' 

912 

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

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

915 means the presentation will switch later. For example: 

916 

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

918 '1 day' 

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

920 '23 hours' 

921 

922 In addition directional information can be provided that informs 

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

924 

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

926 'in 1 hour' 

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

928 '1 hour ago' 

929 

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

931 

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

933 '3 hr' 

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

935 '3h' 

936 

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

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

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

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

941 "hour", "minute" or "second" 

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

943 switches to the next higher unit 

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

945 include directional information. For instance a 

946 positive timedelta will include the information about 

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

948 about the value being in the past. 

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

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

951 maintain compatibility) 

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

953 """ 

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

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

956 if format == 'medium': 

957 warnings.warn( 

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

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

960 category=DeprecationWarning, 

961 stacklevel=2, 

962 ) 

963 format = 'long' 

964 if isinstance(delta, datetime.timedelta): 

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

966 else: 

967 seconds = delta 

968 locale = Locale.parse(locale or LC_TIME) 

969 date_fields = locale._data["date_fields"] 

970 unit_patterns = locale._data["unit_patterns"] 

971 

972 def _iter_patterns(a_unit): 

973 if add_direction: 

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

975 # before falling back to the default. 

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

977 if seconds >= 0: 

978 yield unit_rel_patterns['future'] 

979 else: 

980 yield unit_rel_patterns['past'] 

981 a_unit = f"duration-{a_unit}" 

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

983 yield unit_pats.get(format) 

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

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

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

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

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

989 yield unit_pats.get("short") 

990 

991 for unit, secs_per_unit in TIMEDELTA_UNITS: 

992 value = abs(seconds) / secs_per_unit 

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

994 if unit == granularity and value > 0: 

995 value = max(1, value) 

996 value = int(round(value)) 

997 plural_form = locale.plural_form(value) 

998 pattern = None 

999 for patterns in _iter_patterns(unit): 

1000 if patterns is not None: 

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

1002 if pattern: 

1003 break 

1004 # This really should not happen 

1005 if pattern is None: 

1006 return '' 

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

1008 

1009 return '' 

1010 

1011 

1012def _format_fallback_interval( 

1013 start: _Instant, 

1014 end: _Instant, 

1015 skeleton: str | None, 

1016 tzinfo: datetime.tzinfo | None, 

1017 locale: Locale, 

1018) -> str: 

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

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

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

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

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

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

1025 else: 

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

1027 

1028 formatted_start = format(start) 

1029 formatted_end = format(end) 

1030 

1031 if formatted_start == formatted_end: 

1032 return format(start) 

1033 

1034 return ( 

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

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

1037 replace("{1}", formatted_end) 

1038 ) 

1039 

1040 

1041def format_interval( 

1042 start: _Instant, 

1043 end: _Instant, 

1044 skeleton: str | None = None, 

1045 tzinfo: datetime.tzinfo | None = None, 

1046 fuzzy: bool = True, 

1047 locale: Locale | str | None = None, 

1048) -> str: 

1049 """ 

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

1051 

1052 >>> from datetime import date, time 

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

1054 '15.–17.1.2016' 

1055 

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

1057 '12:12–16:16' 

1058 

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

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

1061 

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

1063 '16:18–16:24' 

1064 

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

1066 

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

1068 '16:18' 

1069 

1070 Unknown skeletons fall back to "default" formatting. 

1071 

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

1073 '2015/01/01~2017/01/01' 

1074 

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

1076 '16:18:00~16:24:00' 

1077 

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

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

1080 

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

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

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

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

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

1086 close enough to it. 

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

1088 :return: Formatted interval 

1089 """ 

1090 locale = Locale.parse(locale or LC_TIME) 

1091 

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

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

1094 

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

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

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

1098 

1099 interval_formats = locale.interval_formats 

1100 

1101 if skeleton not in interval_formats or not skeleton: 

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

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

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

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

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

1107 if skeleton and fuzzy: 

1108 skeleton = match_skeleton(skeleton, interval_formats) 

1109 else: 

1110 skeleton = None 

1111 if not skeleton: # Still no match whatsoever? 

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

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

1114 

1115 skel_formats = interval_formats[skeleton] 

1116 

1117 if start == end: 

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

1119 

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

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

1122 

1123 start_fmt = DateTimeFormat(start, locale=locale) 

1124 end_fmt = DateTimeFormat(end, locale=locale) 

1125 

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

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

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

1129 # > single date using availableFormats, and return. 

1130 

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

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

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

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

1135 return "".join( 

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

1137 for pattern, instant 

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

1139 ) 

1140 

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

1142 

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

1144 

1145 

1146def get_period_id( 

1147 time: _Instant, 

1148 tzinfo: datetime.tzinfo | None = None, 

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

1150 locale: Locale | str | None = None, 

1151) -> str: 

1152 """ 

1153 Get the day period ID for a given time. 

1154 

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

1156 

1157 >>> from datetime import time 

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

1159 'Morgen' 

1160 

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

1162 'midnight' 

1163 

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

1165 'morning1' 

1166 

1167 :param time: The time to inspect. 

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

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

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

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

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

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

1174 """ 

1175 time = _get_time(time, tzinfo) 

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

1177 locale = Locale.parse(locale or LC_TIME) 

1178 

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

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

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

1182 

1183 for rule_id, rules in rulesets: 

1184 for rule in rules: 

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

1186 return rule_id 

1187 

1188 for rule_id, rules in rulesets: 

1189 for rule in rules: 

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

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

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

1193 return rule_id 

1194 else: 

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

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

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

1198 return rule_id 

1199 

1200 start_ok = end_ok = False 

1201 

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

1203 start_ok = True 

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

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

1206 # excuse the lack of test coverage. 

1207 end_ok = True 

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

1209 end_ok = True 

1210 if "after" in rule: 

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

1212 

1213 if start_ok and end_ok: 

1214 return rule_id 

1215 

1216 if seconds_past_midnight < 43200: 

1217 return "am" 

1218 else: 

1219 return "pm" 

1220 

1221 

1222class ParseError(ValueError): 

1223 pass 

1224 

1225 

1226def parse_date( 

1227 string: str, 

1228 locale: Locale | str | None = None, 

1229 format: _PredefinedTimeFormat | str = 'medium', 

1230) -> datetime.date: 

1231 """Parse a date from a string. 

1232 

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

1234 

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

1236 datetime.date(2004, 4, 1) 

1237 

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

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

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

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

1242 the string. 

1243 

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

1245 datetime.date(2004, 4, 1) 

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

1247 datetime.date(2004, 4, 1) 

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

1249 datetime.date(2004, 4, 1) 

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

1251 datetime.date(2004, 4, 1) 

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

1253 datetime.date(2004, 4, 1) 

1254 

1255 :param string: the string containing the date 

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

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

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

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

1260 (see ``get_time_format``) 

1261 """ 

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

1263 if not numbers: 

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

1265 

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

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

1268 # extended YYYY-MM-DD or basic YYYYMMDD 

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

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

1271 if iso_alike and use_predefined_format: 

1272 try: 

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

1274 except ValueError: 

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

1276 

1277 if use_predefined_format: 

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

1279 else: 

1280 fmt = parse_pattern(format) 

1281 format_str = fmt.pattern.lower() 

1282 year_idx = format_str.index('y') 

1283 month_idx = format_str.find('m') 

1284 if month_idx < 0: 

1285 month_idx = format_str.index('l') 

1286 day_idx = format_str.index('d') 

1287 

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

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

1290 

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

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

1293 

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

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

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

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

1298 if month > 12: 

1299 month, day = day, month 

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

1301 

1302 

1303def parse_time( 

1304 string: str, 

1305 locale: Locale | str | None = None, 

1306 format: _PredefinedTimeFormat | str = 'medium', 

1307) -> datetime.time: 

1308 """Parse a time from a string. 

1309 

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

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

1312 

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

1314 the time instead. 

1315 

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

1317 datetime.time(15, 30) 

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

1319 datetime.time(15, 30) 

1320 

1321 :param string: the string containing the time 

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

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

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

1325 (see ``get_time_format``) 

1326 :return: the parsed time 

1327 :rtype: `time` 

1328 """ 

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

1330 if not numbers: 

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

1332 

1333 # TODO: try ISO format first? 

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

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

1336 else: 

1337 fmt = parse_pattern(format) 

1338 format_str = fmt.pattern.lower() 

1339 hour_idx = format_str.find('h') 

1340 if hour_idx < 0: 

1341 hour_idx = format_str.index('k') 

1342 min_idx = format_str.index('m') 

1343 # format might not contain seconds 

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

1345 sec_idx = math.inf 

1346 

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

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

1349 

1350 # TODO: support time zones 

1351 

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

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

1354 hour_offset = 0 

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

1356 hour_offset = 12 

1357 

1358 # Parse up to three numbers from the string. 

1359 minute = second = 0 

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

1361 if len(numbers) > 1: 

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

1363 if len(numbers) > 2: 

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

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

1366 

1367 

1368class DateTimePattern: 

1369 

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

1371 self.pattern = pattern 

1372 self.format = format 

1373 

1374 def __repr__(self) -> str: 

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

1376 

1377 def __str__(self) -> str: 

1378 pat = self.pattern 

1379 return pat 

1380 

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

1382 if not isinstance(other, DateTimeFormat): 

1383 return NotImplemented 

1384 return self.format % other 

1385 

1386 def apply( 

1387 self, 

1388 datetime: datetime.date | datetime.time, 

1389 locale: Locale | str | None, 

1390 reference_date: datetime.date | None = None, 

1391 ) -> str: 

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

1393 

1394 

1395class DateTimeFormat: 

1396 

1397 def __init__( 

1398 self, 

1399 value: datetime.date | datetime.time, 

1400 locale: Locale | str, 

1401 reference_date: datetime.date | None = None, 

1402 ) -> None: 

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

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

1405 value = value.replace(tzinfo=UTC) 

1406 self.value = value 

1407 self.locale = Locale.parse(locale) 

1408 self.reference_date = reference_date 

1409 

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

1411 char = name[0] 

1412 num = len(name) 

1413 if char == 'G': 

1414 return self.format_era(char, num) 

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

1416 return self.format_year(char, num) 

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

1418 return self.format_quarter(char, num) 

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

1420 return self.format_month(char, num) 

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

1422 return self.format_week(char, num) 

1423 elif char == 'd': 

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

1425 elif char == 'D': 

1426 return self.format_day_of_year(num) 

1427 elif char == 'F': 

1428 return self.format_day_of_week_in_month() 

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

1430 return self.format_weekday(char, num) 

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

1432 return self.format_period(char, num) 

1433 elif char == 'h': 

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

1435 return self.format(12, num) 

1436 else: 

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

1438 elif char == 'H': 

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

1440 elif char == 'K': 

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

1442 elif char == 'k': 

1443 if self.value.hour == 0: 

1444 return self.format(24, num) 

1445 else: 

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

1447 elif char == 'm': 

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

1449 elif char == 's': 

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

1451 elif char == 'S': 

1452 return self.format_frac_seconds(num) 

1453 elif char == 'A': 

1454 return self.format_milliseconds_in_day(num) 

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

1456 return self.format_timezone(char, num) 

1457 else: 

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

1459 

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

1461 char = str(char)[0] 

1462 if char == 'y': 

1463 return self.value.year 

1464 elif char == 'M': 

1465 return self.value.month 

1466 elif char == 'd': 

1467 return self.value.day 

1468 elif char == 'H': 

1469 return self.value.hour 

1470 elif char == 'h': 

1471 return self.value.hour % 12 or 12 

1472 elif char == 'm': 

1473 return self.value.minute 

1474 elif char == 'a': 

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

1476 else: 

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

1478 

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

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

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

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

1483 

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

1485 value = self.value.year 

1486 if char.isupper(): 

1487 month = self.value.month 

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

1489 value -= 1 

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

1491 value += 1 

1492 year = self.format(value, num) 

1493 if num == 2: 

1494 year = year[-2:] 

1495 return year 

1496 

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

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

1499 if num <= 2: 

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

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

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

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

1504 

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

1506 if num <= 2: 

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

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

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

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

1511 

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

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

1514 week = self.get_week_of_year() 

1515 return self.format(week, num) 

1516 else: # week of month 

1517 week = self.get_week_of_month() 

1518 return str(week) 

1519 

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

1521 """ 

1522 Return weekday from parsed datetime according to format pattern. 

1523 

1524 >>> from datetime import date 

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

1526 >>> format.format_weekday() 

1527 'Sunday' 

1528 

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

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

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

1532 'Sun' 

1533 

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

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

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

1537 '01' 

1538 

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

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

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

1542 '1' 

1543 

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

1545 :param num: count of format character 

1546 

1547 """ 

1548 if num < 3: 

1549 if char.islower(): 

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

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

1552 num = 3 

1553 weekday = self.value.weekday() 

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

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

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

1557 

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

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

1560 

1561 def format_day_of_week_in_month(self) -> str: 

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

1563 

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

1565 """ 

1566 Return period from parsed datetime according to format pattern. 

1567 

1568 >>> from datetime import datetime, time 

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

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

1571 'ip.' 

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

1573 'iltap.' 

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

1575 'iltapäivä' 

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

1577 'iltapäivällä' 

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

1579 'ip.' 

1580 

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

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

1583 '上午' 

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

1585 '清晨' 

1586 

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

1588 :param num: count of format character 

1589 

1590 """ 

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

1592 'wide', 'narrow', 'abbreviated'] 

1593 if char == 'a': 

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

1595 context = 'format' 

1596 else: 

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

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

1599 for width in widths: 

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

1601 if period in period_names: 

1602 return period_names[period] 

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

1604 

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

1606 """ Return fractional seconds. 

1607 

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

1609 of digits passed in. 

1610 """ 

1611 value = self.value.microsecond / 1000000 

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

1613 

1614 def format_milliseconds_in_day(self, num): 

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

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

1617 return self.format(msecs, num) 

1618 

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

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

1621 

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

1623 # reference date which is important to distinguish between timezone 

1624 # variants (summer/standard time) 

1625 value = self.value 

1626 if self.reference_date: 

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

1628 

1629 if char == 'z': 

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

1631 elif char == 'Z': 

1632 if num == 5: 

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

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

1635 elif char == 'O': 

1636 if num == 4: 

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

1638 # TODO: To add support for O:1 

1639 elif char == 'v': 

1640 return get_timezone_name(value.tzinfo, width, 

1641 locale=self.locale) 

1642 elif char == 'V': 

1643 if num == 1: 

1644 return get_timezone_name(value.tzinfo, width, 

1645 uncommon=True, locale=self.locale) 

1646 elif num == 2: 

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

1648 elif num == 3: 

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

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

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

1652 elif char == 'X': 

1653 if num == 1: 

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

1655 return_z=True) 

1656 elif num in (2, 4): 

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

1658 return_z=True) 

1659 elif num in (3, 5): 

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

1661 return_z=True) 

1662 elif char == 'x': 

1663 if num == 1: 

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

1665 elif num in (2, 4): 

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

1667 elif num in (3, 5): 

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

1669 

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

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

1672 

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

1674 if date is None: 

1675 date = self.value 

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

1677 

1678 def get_week_of_year(self) -> int: 

1679 """Return the week of the year.""" 

1680 day_of_year = self.get_day_of_year(self.value) 

1681 week = self.get_week_number(day_of_year) 

1682 if week == 0: 

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

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

1685 date.weekday()) 

1686 elif week > 52: 

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

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

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

1690 week = 1 

1691 return week 

1692 

1693 def get_week_of_month(self) -> int: 

1694 """Return the week of the month.""" 

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

1696 

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

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

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

1700 

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

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

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

1704 

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

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

1707 1 

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

1709 2 

1710 

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

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

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

1714 current date is assumed 

1715 """ 

1716 if day_of_week is None: 

1717 day_of_week = self.value.weekday() 

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

1719 day_of_period + 1) % 7 

1720 if first_day < 0: 

1721 first_day += 7 

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

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

1724 week_number += 1 

1725 return week_number 

1726 

1727 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1742} 

1743 

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

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

1746#: in order of decreasing magnitude. 

1747PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" 

1748 

1749 

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

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

1752 

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

1754 '%(MMMM)s%(d)s' 

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

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

1757 

1758 Pattern can contain literal strings in single quotes: 

1759 

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

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

1762 

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

1764 characters: 

1765 

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

1767 "%(hh)s o'clock" 

1768 

1769 :param pattern: the formatting pattern to parse 

1770 """ 

1771 if isinstance(pattern, DateTimePattern): 

1772 return pattern 

1773 return _cached_parse_pattern(pattern) 

1774 

1775 

1776@lru_cache(maxsize=1024) 

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

1778 result = [] 

1779 

1780 for tok_type, tok_value in tokenize_pattern(pattern): 

1781 if tok_type == "chars": 

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

1783 elif tok_type == "field": 

1784 fieldchar, fieldnum = tok_value 

1785 limit = PATTERN_CHARS[fieldchar] 

1786 if limit and fieldnum not in limit: 

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

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

1789 else: 

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

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

1792 

1793 

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

1795 """ 

1796 Tokenize date format patterns. 

1797 

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

1799 

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

1801 

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

1803 

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

1805 

1806 :param pattern: Pattern string 

1807 :type pattern: str 

1808 :rtype: list[tuple] 

1809 """ 

1810 result = [] 

1811 quotebuf = None 

1812 charbuf = [] 

1813 fieldchar = [''] 

1814 fieldnum = [0] 

1815 

1816 def append_chars(): 

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

1818 del charbuf[:] 

1819 

1820 def append_field(): 

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

1822 fieldchar[0] = '' 

1823 fieldnum[0] = 0 

1824 

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

1826 if quotebuf is None: 

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

1828 if fieldchar[0]: 

1829 append_field() 

1830 elif charbuf: 

1831 append_chars() 

1832 quotebuf = [] 

1833 elif char in PATTERN_CHARS: 

1834 if charbuf: 

1835 append_chars() 

1836 if char == fieldchar[0]: 

1837 fieldnum[0] += 1 

1838 else: 

1839 if fieldchar[0]: 

1840 append_field() 

1841 fieldchar[0] = char 

1842 fieldnum[0] = 1 

1843 else: 

1844 if fieldchar[0]: 

1845 append_field() 

1846 charbuf.append(char) 

1847 

1848 elif quotebuf is not None: 

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

1850 charbuf.extend(quotebuf) 

1851 quotebuf = None 

1852 else: # inside quote 

1853 quotebuf.append(char) 

1854 

1855 if fieldchar[0]: 

1856 append_field() 

1857 elif charbuf: 

1858 append_chars() 

1859 

1860 return result 

1861 

1862 

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

1864 """ 

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

1866 

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

1868 

1869 :type tokens: Iterable[tuple] 

1870 :rtype: str 

1871 """ 

1872 output = [] 

1873 for tok_type, tok_value in tokens: 

1874 if tok_type == "field": 

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

1876 elif tok_type == "chars": 

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

1878 output.append(tok_value) 

1879 else: 

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

1881 return "".join(output) 

1882 

1883 

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

1885 """ 

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

1887 

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

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

1890 

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

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

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

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

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

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

1897 >>> split_interval_pattern("MMM d") 

1898 ['MMM d'] 

1899 >>> split_interval_pattern("y G") 

1900 ['y G'] 

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

1902 ['MMM d – ', 'd'] 

1903 

1904 :param pattern: Interval pattern string 

1905 :return: list of "subpatterns" 

1906 """ 

1907 

1908 seen_fields = set() 

1909 parts = [[]] 

1910 

1911 for tok_type, tok_value in tokenize_pattern(pattern): 

1912 if tok_type == "field": 

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

1914 parts.append([]) 

1915 seen_fields.clear() 

1916 seen_fields.add(tok_value[0]) 

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

1918 

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

1920 

1921 

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

1923 """ 

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

1925 

1926 This uses the rules outlined in the TR35 document. 

1927 

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

1929 'yMd' 

1930 

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

1932 'jyMMd' 

1933 

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

1935 

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

1937 'hmv' 

1938 

1939 :param skeleton: The skeleton to match 

1940 :type skeleton: str 

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

1942 :type options: Iterable[str] 

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

1944 than the skeleton requested. 

1945 :type allow_different_fields: bool 

1946 

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

1948 :rtype: str|None 

1949 """ 

1950 

1951 # TODO: maybe implement pattern expansion? 

1952 

1953 # Based on the implementation in 

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

1955 

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

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

1958 

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

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

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

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

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

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

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

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

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

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

1969 

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

1971 best_skeleton = None 

1972 best_distance = None 

1973 for option in options: 

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

1975 distance = 0 

1976 for field in PATTERN_CHARS: 

1977 input_width = get_input_field_width(field, 0) 

1978 opt_width = get_opt_field_width(field, 0) 

1979 if input_width == opt_width: 

1980 continue 

1981 if opt_width == 0 or input_width == 0: 

1982 if not allow_different_fields: # This one is not okay 

1983 option = None 

1984 break 

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

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

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

1988 else: 

1989 distance += abs(input_width - opt_width) 

1990 

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

1992 continue 

1993 

1994 if not best_skeleton or distance < best_distance: 

1995 best_skeleton = option 

1996 best_distance = distance 

1997 

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

1999 break 

2000 

2001 return best_skeleton