Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

705 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-2024 by the Babel Team. 

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

16""" 

17 

18from __future__ import annotations 

19 

20import re 

21import warnings 

22from functools import lru_cache 

23from typing import TYPE_CHECKING, SupportsInt 

24 

25try: 

26 import pytz 

27except ModuleNotFoundError: 

28 pytz = None 

29 import zoneinfo 

30 

31import datetime 

32from collections.abc import Iterable 

33 

34from babel import localtime 

35from babel.core import Locale, default_locale, get_global 

36from babel.localedata import LocaleDataDict 

37 

38if TYPE_CHECKING: 

39 from typing_extensions import Literal, TypeAlias 

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

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

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

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

44 

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

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

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

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

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

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

51 

52NO_INHERITANCE_MARKER = '\u2205\u2205\u2205' 

53 

54UTC = datetime.timezone.utc 

55LOCALTZ = localtime.LOCALTZ 

56 

57LC_TIME = default_locale('LC_TIME') 

58 

59 

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

61 # Support localizing with both pytz and zoneinfo tzinfos 

62 # nothing to do 

63 if dt.tzinfo is tz: 

64 return dt 

65 

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

67 return tz.localize(dt) 

68 

69 if dt.tzinfo is None: 

70 # convert naive to localized 

71 return dt.replace(tzinfo=tz) 

72 

73 # convert timezones 

74 return dt.astimezone(tz) 

75 

76 

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

78 """ 

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

80 

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

82 

83 :rtype: tuple[datetime, tzinfo] 

84 """ 

85 if dt_or_tzinfo is None: 

86 dt = datetime.datetime.now() 

87 tzinfo = LOCALTZ 

88 elif isinstance(dt_or_tzinfo, str): 

89 dt = None 

90 tzinfo = get_timezone(dt_or_tzinfo) 

91 elif isinstance(dt_or_tzinfo, int): 

92 dt = None 

93 tzinfo = UTC 

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

95 dt = _get_datetime(dt_or_tzinfo) 

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

97 else: 

98 dt = None 

99 tzinfo = dt_or_tzinfo 

100 return dt, tzinfo 

101 

102 

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

104 """ 

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

106 

107 :rtype: str 

108 """ 

109 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) 

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

111 return tzinfo.zone 

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

113 return tzinfo.key 

114 else: 

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

116 

117 

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

119 """ 

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

121 

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

123 

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

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

126 

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

128 

129 >>> from datetime import date, datetime 

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

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

132 

133 UNIX timestamps are converted to datetimes. 

134 

135 >>> _get_datetime(1400000000) 

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

137 

138 Other values are passed through as-is. 

139 

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

141 >>> _get_datetime(x) is x 

142 True 

143 

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

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

146 :return: a datetime 

147 :rtype: datetime 

148 """ 

149 if instant is None: 

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

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

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

153 elif isinstance(instant, datetime.time): 

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

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

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

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

158 return instant 

159 

160 

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

162 """ 

163 Ensure the datetime passed has an attached tzinfo. 

164 

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

166 

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

168 

169 >>> from datetime import datetime 

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

171 'UTC' 

172 

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

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

175 14 

176 

177 :param datetime: Datetime to augment. 

178 :param tzinfo: optional tzinfo 

179 :return: datetime with tzinfo 

180 :rtype: datetime 

181 """ 

182 if dt.tzinfo is None: 

183 dt = dt.replace(tzinfo=UTC) 

184 if tzinfo is not None: 

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

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

187 dt = tzinfo.normalize(dt) 

188 return dt 

189 

190 

191def _get_time( 

192 time: datetime.time | datetime.datetime | None, 

193 tzinfo: datetime.tzinfo | None = None, 

194) -> datetime.time: 

195 """ 

196 Get a timezoned time from a given instant. 

197 

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

199 

200 :param time: time, datetime or None 

201 :rtype: time 

202 """ 

203 if time is None: 

204 time = datetime.datetime.now(UTC) 

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

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

207 

208 if time.tzinfo is None: 

209 time = time.replace(tzinfo=UTC) 

210 

211 if isinstance(time, datetime.datetime): 

212 if tzinfo is not None: 

213 time = time.astimezone(tzinfo) 

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

215 time = tzinfo.normalize(time) 

216 time = time.timetz() 

217 elif tzinfo is not None: 

218 time = time.replace(tzinfo=tzinfo) 

219 return time 

220 

221 

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

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

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

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

226 the functions of Babel that operate with dates. 

227 

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

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

230 

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

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

233 """ 

234 if zone is None: 

235 return LOCALTZ 

236 if not isinstance(zone, str): 

237 return zone 

238 

239 if pytz: 

240 try: 

241 return pytz.timezone(zone) 

242 except pytz.UnknownTimeZoneError as e: 

243 exc = e 

244 else: 

245 assert zoneinfo 

246 try: 

247 return zoneinfo.ZoneInfo(zone) 

248 except zoneinfo.ZoneInfoNotFoundError as e: 

249 exc = e 

250 

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

252 

253 

254def get_period_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

255 context: _Context = 'stand-alone', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: 

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

257 

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

259 u'AM' 

260 

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

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

263 :param locale: the `Locale` object, or a locale string 

264 """ 

265 return Locale.parse(locale).day_periods[context][width] 

266 

267 

268def get_day_names(width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide', 

269 context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: 

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

271 

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

273 u'Tuesday' 

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

275 u'Tu' 

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

277 u'mar' 

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

279 u'D' 

280 

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

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

283 :param locale: the `Locale` object, or a locale string 

284 """ 

285 return Locale.parse(locale).days[context][width] 

286 

287 

288def get_month_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

289 context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: 

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

291 

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

293 u'January' 

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

295 u'ene' 

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

297 u'J' 

298 

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

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

301 :param locale: the `Locale` object, or a locale string 

302 """ 

303 return Locale.parse(locale).months[context][width] 

304 

305 

306def get_quarter_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

307 context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict: 

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

309 

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

311 u'1st quarter' 

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

313 u'Q1' 

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

315 u'1' 

316 

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

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

319 :param locale: the `Locale` object, or a locale string 

320 """ 

321 return Locale.parse(locale).quarters[context][width] 

322 

323 

324def get_era_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide', 

325 locale: Locale | str | None = LC_TIME) -> LocaleDataDict: 

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

327 

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

329 u'Anno Domini' 

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

331 u'n. Chr.' 

332 

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

334 :param locale: the `Locale` object, or a locale string 

335 """ 

336 return Locale.parse(locale).eras[width] 

337 

338 

339def get_date_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern: 

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

341 format. 

342 

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

344 <DateTimePattern u'MMM d, y'> 

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

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

347 

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

349 "short" 

350 :param locale: the `Locale` object, or a locale string 

351 """ 

352 return Locale.parse(locale).date_formats[format] 

353 

354 

355def get_datetime_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern: 

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

357 specified format. 

358 

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

360 u'{1}, {0}' 

361 

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

363 "short" 

364 :param locale: the `Locale` object, or a locale string 

365 """ 

366 patterns = Locale.parse(locale).datetime_formats 

367 if format not in patterns: 

368 format = None 

369 return patterns[format] 

370 

371 

372def get_time_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern: 

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

374 format. 

375 

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

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

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

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

380 

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

382 "short" 

383 :param locale: the `Locale` object, or a locale string 

384 """ 

385 return Locale.parse(locale).time_formats[format] 

386 

387 

388def get_timezone_gmt( 

389 datetime: _Instant = None, 

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

391 locale: Locale | str | None = LC_TIME, 

392 return_z: bool = False, 

393) -> str: 

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

395 as string indicating the offset from GMT. 

396 

397 >>> from datetime import datetime 

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

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

400 u'GMT+00:00' 

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

402 'Z' 

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

404 u'+00' 

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

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

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

408 u'GMT-07:00' 

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

410 u'-0700' 

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

412 u'-07' 

413 

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

415 UTC string is used instead of GMT: 

416 

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

418 u'UTC-07:00' 

419 

420 .. versionadded:: 0.9 

421 

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

423 time in UTC is used 

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

425 :param locale: the `Locale` object, or a locale string 

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

427 when local time offset is 0 

428 """ 

429 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime)) 

430 locale = Locale.parse(locale) 

431 

432 offset = datetime.tzinfo.utcoffset(datetime) 

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

434 hours, seconds = divmod(seconds, 3600) 

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

436 return 'Z' 

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

438 return '%+03d' % hours 

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

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

441 elif width == 'iso8601': 

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

443 else: 

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

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

446 

447 

448def get_timezone_location( 

449 dt_or_tzinfo: _DtOrTzinfo = None, 

450 locale: Locale | str | None = LC_TIME, 

451 return_city: bool = False, 

452) -> str: 

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

454 

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

456 city associated with the time zone: 

457 

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

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

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

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

462 Canada (St. John’s) Time 

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

464 St. John’s 

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

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

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

468 

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

470 timezone, just the localized country name is returned: 

471 

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

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

474 u'Mitteleurop\\xe4ische Zeit' 

475 

476 .. versionadded:: 0.9 

477 

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

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

480 UTC is assumed 

481 :param locale: the `Locale` object, or a locale string 

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

483 for the time zone 

484 :return: the localized timezone name using location format 

485 

486 """ 

487 locale = Locale.parse(locale) 

488 

489 zone = _get_tz_name(dt_or_tzinfo) 

490 

491 # Get the canonical time-zone code 

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

493 

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

495 

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

497 # localized country name 

498 region_format = locale.zone_formats['region'] 

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

500 if territory not in locale.territories: 

501 territory = 'ZZ' # invalid/unknown 

502 territory_name = locale.territories[territory] 

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

504 return region_format % territory_name 

505 

506 # Otherwise, include the city in the output 

507 fallback_format = locale.zone_formats['fallback'] 

508 if 'city' in info: 

509 city_name = info['city'] 

510 else: 

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

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

513 if 'city' in metazone_info: 

514 city_name = metazone_info['city'] 

515 elif '/' in zone: 

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

517 else: 

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

519 

520 if return_city: 

521 return city_name 

522 return region_format % (fallback_format % { 

523 '0': city_name, 

524 '1': territory_name, 

525 }) 

526 

527 

528def get_timezone_name( 

529 dt_or_tzinfo: _DtOrTzinfo = None, 

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

531 uncommon: bool = False, 

532 locale: Locale | str | None = LC_TIME, 

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

534 return_zone: bool = False, 

535) -> str: 

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

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

538 

539 >>> from datetime import time 

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

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

542 u'Pacific Standard Time' 

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

544 'America/Los_Angeles' 

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

546 u'PST' 

547 

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

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

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

551 time of events that recur across DST changes: 

552 

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

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

555 u'Pacific Time' 

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

557 u'PT' 

558 

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

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

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

562 

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

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

565 u'Mitteleurop\xe4ische Zeit' 

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

567 u'Hor\xe1rio da Europa Central' 

568 

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

570 included in the representation: 

571 

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

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

574 u'Neufundland-Zeit' 

575 

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

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

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

579 format. 

580 

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

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

583 

584 .. versionadded:: 0.9 

585 

586 .. versionchanged:: 1.0 

587 Added `zone_variant` support. 

588 

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

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

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

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

593 current date in UTC is assumed 

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

595 :param uncommon: deprecated and ignored 

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

597 variation is defined from the datetime object 

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

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

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

601 ``'standard'``. 

602 :param locale: the `Locale` object, or a locale string 

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

604 returns long time zone ID 

605 """ 

606 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) 

607 locale = Locale.parse(locale) 

608 

609 zone = _get_tz_name(dt_or_tzinfo) 

610 

611 if zone_variant is None: 

612 if dt is None: 

613 zone_variant = 'generic' 

614 else: 

615 dst = tzinfo.dst(dt) 

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

617 else: 

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

619 raise ValueError('Invalid zone variation') 

620 

621 # Get the canonical time-zone code 

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

623 if return_zone: 

624 return zone 

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

626 # Try explicitly translated zone names first 

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

628 return info[width][zone_variant] 

629 

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

631 if metazone: 

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

633 if width in metazone_info: 

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

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

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

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

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

639 if name: 

640 return name 

641 

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

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

644 if dt is not None: 

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

646 

647 return get_timezone_location(dt_or_tzinfo, locale=locale) 

648 

649 

650def format_date( 

651 date: datetime.date | None = None, 

652 format: _PredefinedTimeFormat | str = 'medium', 

653 locale: Locale | str | None = LC_TIME, 

654) -> str: 

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

656 

657 >>> from datetime import date 

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

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

660 u'Apr 1, 2007' 

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

662 u'Sonntag, 1. April 2007' 

663 

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

665 custom date pattern: 

666 

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

668 u"Sun, Apr 1, '07" 

669 

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

671 date is used 

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

673 date/time pattern 

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

675 """ 

676 if date is None: 

677 date = datetime.date.today() 

678 elif isinstance(date, datetime.datetime): 

679 date = date.date() 

680 

681 locale = Locale.parse(locale) 

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

683 format = get_date_format(format, locale=locale) 

684 pattern = parse_pattern(format) 

685 return pattern.apply(date, locale) 

686 

687 

688def format_datetime( 

689 datetime: _Instant = None, 

690 format: _PredefinedTimeFormat | str = 'medium', 

691 tzinfo: datetime.tzinfo | None = None, 

692 locale: Locale | str | None = LC_TIME, 

693) -> str: 

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

695 

696 >>> from datetime import datetime 

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

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

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

700 

701 For any pattern requiring the display of the timezone: 

702 

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

704 ... locale='fr_FR') 

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

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

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

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

709 

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

711 time is used 

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

713 date/time pattern 

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

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

716 """ 

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

718 

719 locale = Locale.parse(locale) 

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

721 return get_datetime_format(format, locale=locale) \ 

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

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

724 locale=locale)) \ 

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

726 else: 

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

728 

729 

730def format_time( 

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

732 format: _PredefinedTimeFormat | str = 'medium', 

733 tzinfo: datetime.tzinfo | None = None, locale: Locale | str | None = LC_TIME, 

734) -> str: 

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

736 

737 >>> from datetime import datetime, time 

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

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

740 u'3:30:00\u202fPM' 

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

742 u'15:30' 

743 

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

745 custom time pattern: 

746 

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

748 u"03 o'clock PM" 

749 

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

751 timezone has to be specified explicitly: 

752 

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

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

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

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

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

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

759 ... locale='en') 

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

761 

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

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

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

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

766 it is assumed to be in UTC. 

767 

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

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

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

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

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

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

774 

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

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

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

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

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

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

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

782 

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

784 time in UTC is used 

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

786 date/time pattern 

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

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

789 """ 

790 

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

792 # in the pattern 

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

794 

795 time = _get_time(time, tzinfo) 

796 

797 locale = Locale.parse(locale) 

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

799 format = get_time_format(format, locale=locale) 

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

801 

802 

803def format_skeleton( 

804 skeleton: str, 

805 datetime: _Instant = None, 

806 tzinfo: datetime.tzinfo | None = None, 

807 fuzzy: bool = True, 

808 locale: Locale | str | None = LC_TIME, 

809) -> str: 

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

811 

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

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

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

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

816 

817 >>> from datetime import datetime 

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

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

820 u'dim. 1 avr.' 

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

822 u'Sun, Apr 1' 

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

824 u'1.4.2007' 

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

826 Traceback (most recent call last): 

827 ... 

828 KeyError: yMMd 

829 

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

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

832 

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

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

835 time in UTC is used 

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

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

838 close enough to it. 

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

840 """ 

841 locale = Locale.parse(locale) 

842 if fuzzy and skeleton not in locale.datetime_skeletons: 

843 skeleton = match_skeleton(skeleton, locale.datetime_skeletons) 

844 format = locale.datetime_skeletons[skeleton] 

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

846 

847 

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

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

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

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

852 ('day', 3600 * 24), 

853 ('hour', 3600), 

854 ('minute', 60), 

855 ('second', 1), 

856) 

857 

858 

859def format_timedelta( 

860 delta: datetime.timedelta | int, 

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

862 threshold: float = .85, 

863 add_direction: bool = False, 

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

865 locale: Locale | str | None = LC_TIME, 

866) -> str: 

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

868 

869 >>> from datetime import timedelta 

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

871 u'3 months' 

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

873 u'1 segundo' 

874 

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

876 presented, which defaults to a second. 

877 

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

879 u'1 day' 

880 

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

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

883 means the presentation will switch later. For example: 

884 

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

886 u'1 day' 

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

888 u'23 hours' 

889 

890 In addition directional information can be provided that informs 

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

892 

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

894 u'in 1 hour' 

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

896 u'1 hour ago' 

897 

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

899 

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

901 u'3 hr' 

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

903 u'3h' 

904 

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

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

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

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

909 "hour", "minute" or "second" 

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

911 switches to the next higher unit 

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

913 include directional information. For instance a 

914 positive timedelta will include the information about 

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

916 about the value being in the past. 

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

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

919 maintain compatibility) 

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

921 """ 

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

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

924 if format == 'medium': 

925 warnings.warn( 

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

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

928 category=DeprecationWarning, 

929 stacklevel=2, 

930 ) 

931 format = 'long' 

932 if isinstance(delta, datetime.timedelta): 

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

934 else: 

935 seconds = delta 

936 locale = Locale.parse(locale) 

937 

938 def _iter_patterns(a_unit): 

939 if add_direction: 

940 unit_rel_patterns = locale._data['date_fields'][a_unit] 

941 if seconds >= 0: 

942 yield unit_rel_patterns['future'] 

943 else: 

944 yield unit_rel_patterns['past'] 

945 a_unit = f"duration-{a_unit}" 

946 unit_pats = locale._data['unit_patterns'].get(a_unit, {}) 

947 yield unit_pats.get(format) 

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

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

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

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

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

953 yield unit_pats.get("short") 

954 

955 for unit, secs_per_unit in TIMEDELTA_UNITS: 

956 value = abs(seconds) / secs_per_unit 

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

958 if unit == granularity and value > 0: 

959 value = max(1, value) 

960 value = int(round(value)) 

961 plural_form = locale.plural_form(value) 

962 pattern = None 

963 for patterns in _iter_patterns(unit): 

964 if patterns is not None: 

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

966 if pattern: 

967 break 

968 # This really should not happen 

969 if pattern is None: 

970 return '' 

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

972 

973 return '' 

974 

975 

976def _format_fallback_interval( 

977 start: _Instant, 

978 end: _Instant, 

979 skeleton: str | None, 

980 tzinfo: datetime.tzinfo | None, 

981 locale: Locale | str | None = LC_TIME, 

982) -> str: 

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

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

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

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

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

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

989 else: 

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

991 

992 formatted_start = format(start) 

993 formatted_end = format(end) 

994 

995 if formatted_start == formatted_end: 

996 return format(start) 

997 

998 return ( 

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

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

1001 replace("{1}", formatted_end) 

1002 ) 

1003 

1004 

1005def format_interval( 

1006 start: _Instant, 

1007 end: _Instant, 

1008 skeleton: str | None = None, 

1009 tzinfo: datetime.tzinfo | None = None, 

1010 fuzzy: bool = True, 

1011 locale: Locale | str | None = LC_TIME, 

1012) -> str: 

1013 """ 

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

1015 

1016 >>> from datetime import date, time 

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

1018 u'15.\u201317.1.2016' 

1019 

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

1021 '12:12\u201316:16' 

1022 

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

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

1025 

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

1027 '16:18\u201316:24' 

1028 

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

1030 

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

1032 '16:18' 

1033 

1034 Unknown skeletons fall back to "default" formatting. 

1035 

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

1037 '2015/01/01\uff5e2017/01/01' 

1038 

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

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

1041 

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

1043 '15.01.2016\u2009–\u200917.01.2016' 

1044 

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

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

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

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

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

1050 close enough to it. 

1051 :param locale: A locale object or identifier. 

1052 :return: Formatted interval 

1053 """ 

1054 locale = Locale.parse(locale) 

1055 

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

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

1058 

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

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

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

1062 

1063 interval_formats = locale.interval_formats 

1064 

1065 if skeleton not in interval_formats or not skeleton: 

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

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

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

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

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

1071 if skeleton and fuzzy: 

1072 skeleton = match_skeleton(skeleton, interval_formats) 

1073 else: 

1074 skeleton = None 

1075 if not skeleton: # Still no match whatsoever? 

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

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

1078 

1079 skel_formats = interval_formats[skeleton] 

1080 

1081 if start == end: 

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

1083 

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

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

1086 

1087 start_fmt = DateTimeFormat(start, locale=locale) 

1088 end_fmt = DateTimeFormat(end, locale=locale) 

1089 

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

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

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

1093 # > single date using availableFormats, and return. 

1094 

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

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

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

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

1099 return "".join( 

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

1101 for pattern, instant 

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

1103 ) 

1104 

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

1106 

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

1108 

1109 

1110def get_period_id( 

1111 time: _Instant, 

1112 tzinfo: datetime.tzinfo | None = None, 

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

1114 locale: Locale | str | None = LC_TIME, 

1115) -> str: 

1116 """ 

1117 Get the day period ID for a given time. 

1118 

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

1120 

1121 >>> from datetime import time 

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

1123 u'Morgen' 

1124 

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

1126 u'midnight' 

1127 

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

1129 u'night1' 

1130 

1131 :param time: The time to inspect. 

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

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

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

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

1136 :param locale: the `Locale` object, or a locale string 

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

1138 """ 

1139 time = _get_time(time, tzinfo) 

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

1141 locale = Locale.parse(locale) 

1142 

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

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

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

1146 

1147 for rule_id, rules in rulesets: 

1148 for rule in rules: 

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

1150 return rule_id 

1151 

1152 for rule_id, rules in rulesets: 

1153 for rule in rules: 

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

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

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

1157 return rule_id 

1158 else: 

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

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

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

1162 return rule_id 

1163 

1164 start_ok = end_ok = False 

1165 

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

1167 start_ok = True 

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

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

1170 # excuse the lack of test coverage. 

1171 end_ok = True 

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

1173 end_ok = True 

1174 if "after" in rule: 

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

1176 

1177 if start_ok and end_ok: 

1178 return rule_id 

1179 

1180 if seconds_past_midnight < 43200: 

1181 return "am" 

1182 else: 

1183 return "pm" 

1184 

1185 

1186class ParseError(ValueError): 

1187 pass 

1188 

1189 

1190def parse_date( 

1191 string: str, 

1192 locale: Locale | str | None = LC_TIME, 

1193 format: _PredefinedTimeFormat = 'medium', 

1194) -> datetime.date: 

1195 """Parse a date from a string. 

1196 

1197 This function first tries to interpret the string as ISO-8601 

1198 date format, then uses the date format for the locale as a hint to 

1199 determine the order in which the date fields appear in the string. 

1200 

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

1202 datetime.date(2004, 4, 1) 

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

1204 datetime.date(2004, 4, 1) 

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

1206 datetime.date(2004, 4, 1) 

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

1208 datetime.date(2004, 4, 1) 

1209 

1210 :param string: the string containing the date 

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

1212 :param format: the format to use (see ``get_date_format``) 

1213 """ 

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

1215 if not numbers: 

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

1217 

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

1219 # extended YYYY-MM-DD or basic YYYYMMDD 

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

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

1222 if iso_alike: 

1223 try: 

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

1225 except ValueError: 

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

1227 

1228 format_str = get_date_format(format=format, locale=locale).pattern.lower() 

1229 year_idx = format_str.index('y') 

1230 month_idx = format_str.index('m') 

1231 if month_idx < 0: 

1232 month_idx = format_str.index('l') 

1233 day_idx = format_str.index('d') 

1234 

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

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

1237 

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

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

1240 

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

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

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

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

1245 if month > 12: 

1246 month, day = day, month 

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

1248 

1249 

1250def parse_time( 

1251 string: str, 

1252 locale: Locale | str | None = LC_TIME, 

1253 format: _PredefinedTimeFormat = 'medium', 

1254) -> datetime.time: 

1255 """Parse a time from a string. 

1256 

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

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

1259 

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

1261 datetime.time(15, 30) 

1262 

1263 :param string: the string containing the time 

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

1265 :param format: the format to use (see ``get_time_format``) 

1266 :return: the parsed time 

1267 :rtype: `time` 

1268 """ 

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

1270 if not numbers: 

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

1272 

1273 # TODO: try ISO format first? 

1274 format_str = get_time_format(format=format, locale=locale).pattern.lower() 

1275 hour_idx = format_str.index('h') 

1276 if hour_idx < 0: 

1277 hour_idx = format_str.index('k') 

1278 min_idx = format_str.index('m') 

1279 sec_idx = format_str.index('s') 

1280 

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

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

1283 

1284 # TODO: support time zones 

1285 

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

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

1288 hour_offset = 0 

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

1290 hour_offset = 12 

1291 

1292 # Parse up to three numbers from the string. 

1293 minute = second = 0 

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

1295 if len(numbers) > 1: 

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

1297 if len(numbers) > 2: 

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

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

1300 

1301 

1302class DateTimePattern: 

1303 

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

1305 self.pattern = pattern 

1306 self.format = format 

1307 

1308 def __repr__(self) -> str: 

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

1310 

1311 def __str__(self) -> str: 

1312 pat = self.pattern 

1313 return pat 

1314 

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

1316 if not isinstance(other, DateTimeFormat): 

1317 return NotImplemented 

1318 return self.format % other 

1319 

1320 def apply( 

1321 self, 

1322 datetime: datetime.date | datetime.time, 

1323 locale: Locale | str | None, 

1324 reference_date: datetime.date | None = None, 

1325 ) -> str: 

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

1327 

1328 

1329class DateTimeFormat: 

1330 

1331 def __init__( 

1332 self, 

1333 value: datetime.date | datetime.time, 

1334 locale: Locale | str, 

1335 reference_date: datetime.date | None = None, 

1336 ) -> None: 

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

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

1339 value = value.replace(tzinfo=UTC) 

1340 self.value = value 

1341 self.locale = Locale.parse(locale) 

1342 self.reference_date = reference_date 

1343 

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

1345 char = name[0] 

1346 num = len(name) 

1347 if char == 'G': 

1348 return self.format_era(char, num) 

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

1350 return self.format_year(char, num) 

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

1352 return self.format_quarter(char, num) 

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

1354 return self.format_month(char, num) 

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

1356 return self.format_week(char, num) 

1357 elif char == 'd': 

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

1359 elif char == 'D': 

1360 return self.format_day_of_year(num) 

1361 elif char == 'F': 

1362 return self.format_day_of_week_in_month() 

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

1364 return self.format_weekday(char, num) 

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

1366 return self.format_period(char, num) 

1367 elif char == 'h': 

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

1369 return self.format(12, num) 

1370 else: 

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

1372 elif char == 'H': 

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

1374 elif char == 'K': 

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

1376 elif char == 'k': 

1377 if self.value.hour == 0: 

1378 return self.format(24, num) 

1379 else: 

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

1381 elif char == 'm': 

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

1383 elif char == 's': 

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

1385 elif char == 'S': 

1386 return self.format_frac_seconds(num) 

1387 elif char == 'A': 

1388 return self.format_milliseconds_in_day(num) 

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

1390 return self.format_timezone(char, num) 

1391 else: 

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

1393 

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

1395 char = str(char)[0] 

1396 if char == 'y': 

1397 return self.value.year 

1398 elif char == 'M': 

1399 return self.value.month 

1400 elif char == 'd': 

1401 return self.value.day 

1402 elif char == 'H': 

1403 return self.value.hour 

1404 elif char == 'h': 

1405 return self.value.hour % 12 or 12 

1406 elif char == 'm': 

1407 return self.value.minute 

1408 elif char == 'a': 

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

1410 else: 

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

1412 

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

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

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

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

1417 

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

1419 value = self.value.year 

1420 if char.isupper(): 

1421 value = self.value.isocalendar()[0] 

1422 year = self.format(value, num) 

1423 if num == 2: 

1424 year = year[-2:] 

1425 return year 

1426 

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

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

1429 if num <= 2: 

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

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

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

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

1434 

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

1436 if num <= 2: 

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

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

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

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

1441 

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

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

1444 day_of_year = self.get_day_of_year() 

1445 week = self.get_week_number(day_of_year) 

1446 if week == 0: 

1447 date = self.value - datetime.timedelta(days=day_of_year) 

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

1449 date.weekday()) 

1450 return self.format(week, num) 

1451 else: # week of month 

1452 week = self.get_week_number(self.value.day) 

1453 if week == 0: 

1454 date = self.value - datetime.timedelta(days=self.value.day) 

1455 week = self.get_week_number(date.day, date.weekday()) 

1456 return str(week) 

1457 

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

1459 """ 

1460 Return weekday from parsed datetime according to format pattern. 

1461 

1462 >>> from datetime import date 

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

1464 >>> format.format_weekday() 

1465 u'Sunday' 

1466 

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

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

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

1470 u'Sun' 

1471 

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

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

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

1475 '01' 

1476 

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

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

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

1480 '1' 

1481 

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

1483 :param num: count of format character 

1484 

1485 """ 

1486 if num < 3: 

1487 if char.islower(): 

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

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

1490 num = 3 

1491 weekday = self.value.weekday() 

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

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

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

1495 

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

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

1498 

1499 def format_day_of_week_in_month(self) -> str: 

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

1501 

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

1503 """ 

1504 Return period from parsed datetime according to format pattern. 

1505 

1506 >>> from datetime import datetime, time 

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

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

1509 u'ip.' 

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

1511 u'iltap.' 

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

1513 u'iltapäivä' 

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

1515 u'iltapäivällä' 

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

1517 u'ip.' 

1518 

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

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

1521 u'上午' 

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

1523 u'清晨' 

1524 

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

1526 :param num: count of format character 

1527 

1528 """ 

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

1530 'wide', 'narrow', 'abbreviated'] 

1531 if char == 'a': 

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

1533 context = 'format' 

1534 else: 

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

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

1537 for width in widths: 

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

1539 if period in period_names: 

1540 return period_names[period] 

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

1542 

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

1544 """ Return fractional seconds. 

1545 

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

1547 of digits passed in. 

1548 """ 

1549 value = self.value.microsecond / 1000000 

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

1551 

1552 def format_milliseconds_in_day(self, num): 

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

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

1555 return self.format(msecs, num) 

1556 

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

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

1559 

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

1561 # reference date which is important to distinguish between timezone 

1562 # variants (summer/standard time) 

1563 value = self.value 

1564 if self.reference_date: 

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

1566 

1567 if char == 'z': 

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

1569 elif char == 'Z': 

1570 if num == 5: 

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

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

1573 elif char == 'O': 

1574 if num == 4: 

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

1576 # TODO: To add support for O:1 

1577 elif char == 'v': 

1578 return get_timezone_name(value.tzinfo, width, 

1579 locale=self.locale) 

1580 elif char == 'V': 

1581 if num == 1: 

1582 return get_timezone_name(value.tzinfo, width, 

1583 uncommon=True, locale=self.locale) 

1584 elif num == 2: 

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

1586 elif num == 3: 

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

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

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

1590 elif char == 'X': 

1591 if num == 1: 

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

1593 return_z=True) 

1594 elif num in (2, 4): 

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

1596 return_z=True) 

1597 elif num in (3, 5): 

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

1599 return_z=True) 

1600 elif char == 'x': 

1601 if num == 1: 

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

1603 elif num in (2, 4): 

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

1605 elif num in (3, 5): 

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

1607 

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

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

1610 

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

1612 if date is None: 

1613 date = self.value 

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

1615 

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

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

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

1619 

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

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

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

1623 

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

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

1626 1 

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

1628 2 

1629 

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

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

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

1633 current date is assumed 

1634 """ 

1635 if day_of_week is None: 

1636 day_of_week = self.value.weekday() 

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

1638 day_of_period + 1) % 7 

1639 if first_day < 0: 

1640 first_day += 7 

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

1642 

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

1644 week_number += 1 

1645 

1646 if self.locale.first_week_day == 0: 

1647 # Correct the weeknumber in case of iso-calendar usage (first_week_day=0). 

1648 # If the weeknumber exceeds the maximum number of weeks for the given year 

1649 # we must count from zero.For example the above calculation gives week 53 

1650 # for 2018-12-31. By iso-calender definition 2018 has a max of 52 

1651 # weeks, thus the weeknumber must be 53-52=1. 

1652 max_weeks = datetime.date(year=self.value.year, day=28, month=12).isocalendar()[1] 

1653 if week_number > max_weeks: 

1654 week_number -= max_weeks 

1655 

1656 return week_number 

1657 

1658 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1673} 

1674 

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

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

1677#: in order of decreasing magnitude. 

1678PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" 

1679 

1680 

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

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

1683 

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

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

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

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

1688 

1689 Pattern can contain literal strings in single quotes: 

1690 

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

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

1693 

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

1695 characters: 

1696 

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

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

1699 

1700 :param pattern: the formatting pattern to parse 

1701 """ 

1702 if isinstance(pattern, DateTimePattern): 

1703 return pattern 

1704 return _cached_parse_pattern(pattern) 

1705 

1706 

1707@lru_cache(maxsize=1024) 

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

1709 result = [] 

1710 

1711 for tok_type, tok_value in tokenize_pattern(pattern): 

1712 if tok_type == "chars": 

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

1714 elif tok_type == "field": 

1715 fieldchar, fieldnum = tok_value 

1716 limit = PATTERN_CHARS[fieldchar] 

1717 if limit and fieldnum not in limit: 

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

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

1720 else: 

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

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

1723 

1724 

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

1726 """ 

1727 Tokenize date format patterns. 

1728 

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

1730 

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

1732 

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

1734 

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

1736 

1737 :param pattern: Pattern string 

1738 :type pattern: str 

1739 :rtype: list[tuple] 

1740 """ 

1741 result = [] 

1742 quotebuf = None 

1743 charbuf = [] 

1744 fieldchar = [''] 

1745 fieldnum = [0] 

1746 

1747 def append_chars(): 

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

1749 del charbuf[:] 

1750 

1751 def append_field(): 

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

1753 fieldchar[0] = '' 

1754 fieldnum[0] = 0 

1755 

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

1757 if quotebuf is None: 

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

1759 if fieldchar[0]: 

1760 append_field() 

1761 elif charbuf: 

1762 append_chars() 

1763 quotebuf = [] 

1764 elif char in PATTERN_CHARS: 

1765 if charbuf: 

1766 append_chars() 

1767 if char == fieldchar[0]: 

1768 fieldnum[0] += 1 

1769 else: 

1770 if fieldchar[0]: 

1771 append_field() 

1772 fieldchar[0] = char 

1773 fieldnum[0] = 1 

1774 else: 

1775 if fieldchar[0]: 

1776 append_field() 

1777 charbuf.append(char) 

1778 

1779 elif quotebuf is not None: 

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

1781 charbuf.extend(quotebuf) 

1782 quotebuf = None 

1783 else: # inside quote 

1784 quotebuf.append(char) 

1785 

1786 if fieldchar[0]: 

1787 append_field() 

1788 elif charbuf: 

1789 append_chars() 

1790 

1791 return result 

1792 

1793 

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

1795 """ 

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

1797 

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

1799 

1800 :type tokens: Iterable[tuple] 

1801 :rtype: str 

1802 """ 

1803 output = [] 

1804 for tok_type, tok_value in tokens: 

1805 if tok_type == "field": 

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

1807 elif tok_type == "chars": 

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

1809 output.append(tok_value) 

1810 else: 

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

1812 return "".join(output) 

1813 

1814 

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

1816 """ 

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

1818 

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

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

1821 

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

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

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

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

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

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

1828 >>> split_interval_pattern("MMM d") 

1829 ['MMM d'] 

1830 >>> split_interval_pattern("y G") 

1831 ['y G'] 

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

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

1834 

1835 :param pattern: Interval pattern string 

1836 :return: list of "subpatterns" 

1837 """ 

1838 

1839 seen_fields = set() 

1840 parts = [[]] 

1841 

1842 for tok_type, tok_value in tokenize_pattern(pattern): 

1843 if tok_type == "field": 

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

1845 parts.append([]) 

1846 seen_fields.clear() 

1847 seen_fields.add(tok_value[0]) 

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

1849 

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

1851 

1852 

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

1854 """ 

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

1856 

1857 This uses the rules outlined in the TR35 document. 

1858 

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

1860 'yMd' 

1861 

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

1863 'jyMMd' 

1864 

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

1866 

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

1868 'hmv' 

1869 

1870 :param skeleton: The skeleton to match 

1871 :type skeleton: str 

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

1873 :type options: Iterable[str] 

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

1875 :rtype: str|None 

1876 """ 

1877 

1878 # TODO: maybe implement pattern expansion? 

1879 

1880 # Based on the implementation in 

1881 # http://source.icu-project.org/repos/icu/icu4j/trunk/main/classes/core/src/com/ibm/icu/text/DateIntervalInfo.java 

1882 

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

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

1885 

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

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

1888 

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

1890 best_skeleton = None 

1891 best_distance = None 

1892 for option in options: 

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

1894 distance = 0 

1895 for field in PATTERN_CHARS: 

1896 input_width = get_input_field_width(field, 0) 

1897 opt_width = get_opt_field_width(field, 0) 

1898 if input_width == opt_width: 

1899 continue 

1900 if opt_width == 0 or input_width == 0: 

1901 if not allow_different_fields: # This one is not okay 

1902 option = None 

1903 break 

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

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

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

1907 else: 

1908 distance += abs(input_width - opt_width) 

1909 

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

1911 continue 

1912 

1913 if not best_skeleton or distance < best_distance: 

1914 best_skeleton = option 

1915 best_distance = distance 

1916 

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

1918 break 

1919 

1920 return best_skeleton