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

746 statements  

1""" 

2babel.dates 

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

4 

5Locale dependent formatting and parsing of dates and times. 

6 

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

8following environment variables, in that order: 

9 

10 * ``LC_TIME``, 

11 * ``LC_ALL``, and 

12 * ``LANG`` 

13 

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

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

16""" 

17 

18from __future__ import annotations 

19 

20import math 

21import re 

22import warnings 

23from functools import lru_cache 

24from typing import TYPE_CHECKING, Literal, SupportsInt 

25 

26try: 

27 import pytz 

28except ModuleNotFoundError: 

29 pytz = None 

30 import zoneinfo 

31 

32import datetime 

33from collections.abc import Iterable 

34 

35from babel import localtime 

36from babel.core import Locale, default_locale, get_global 

37from babel.localedata import LocaleDataDict 

38 

39if TYPE_CHECKING: 

40 from typing_extensions import TypeAlias 

41 

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

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

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

45 _DtOrTzinfo: TypeAlias = datetime.datetime | datetime.tzinfo | str | int | datetime.time | None # fmt: skip 

46 

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

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

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

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

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

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

53 

54NO_INHERITANCE_MARKER = '\u2205\u2205\u2205' 

55 

56UTC = datetime.timezone.utc 

57LOCALTZ = localtime.LOCALTZ 

58 

59LC_TIME = default_locale('LC_TIME') 

60 

61 

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

63 # Support localizing with both pytz and zoneinfo tzinfos 

64 # nothing to do 

65 if dt.tzinfo is tz: 

66 return dt 

67 

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

69 return tz.localize(dt) 

70 

71 if dt.tzinfo is None: 

72 # convert naive to localized 

73 return dt.replace(tzinfo=tz) 

74 

75 # convert timezones 

76 return dt.astimezone(tz) 

77 

78 

79def _get_dt_and_tzinfo( 

80 dt_or_tzinfo: _DtOrTzinfo, 

81) -> tuple[datetime.datetime | None, datetime.tzinfo]: 

82 """ 

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

84 

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

86 

87 :rtype: tuple[datetime, tzinfo] 

88 """ 

89 if dt_or_tzinfo is None: 

90 dt = datetime.datetime.now() 

91 tzinfo = LOCALTZ 

92 elif isinstance(dt_or_tzinfo, str): 

93 dt = None 

94 tzinfo = get_timezone(dt_or_tzinfo) 

95 elif isinstance(dt_or_tzinfo, int): 

96 dt = None 

97 tzinfo = UTC 

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

99 dt = _get_datetime(dt_or_tzinfo) 

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

101 else: 

102 dt = None 

103 tzinfo = dt_or_tzinfo 

104 return dt, tzinfo 

105 

106 

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

108 """ 

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

110 

111 :rtype: str 

112 """ 

113 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) 

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

115 return tzinfo.zone 

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

117 return tzinfo.key 

118 else: 

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

120 

121 

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

123 """ 

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

125 

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

127 

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

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

130 

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

132 

133 >>> from datetime import date, datetime 

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

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

136 

137 UNIX timestamps are converted to datetimes. 

138 

139 >>> _get_datetime(1400000000) 

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

141 

142 Other values are passed through as-is. 

143 

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

145 >>> _get_datetime(x) is x 

146 True 

147 

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

149 :return: a datetime 

150 """ 

151 if instant is None: 

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

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

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

155 elif isinstance(instant, datetime.time): 

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

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

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

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

160 return instant 

161 

162 

163def _ensure_datetime_tzinfo( 

164 dt: datetime.datetime, 

165 tzinfo: datetime.tzinfo | None = None, 

166) -> datetime.datetime: 

167 """ 

168 Ensure the datetime passed has an attached tzinfo. 

169 

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

171 

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

173 

174 >>> from datetime import datetime 

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

176 'UTC' 

177 

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

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

180 14 

181 

182 :param datetime: Datetime to augment. 

183 :param tzinfo: optional tzinfo 

184 :return: datetime with tzinfo 

185 :rtype: datetime 

186 """ 

187 if dt.tzinfo is None: 

188 dt = dt.replace(tzinfo=UTC) 

189 if tzinfo is not None: 

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

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

192 dt = tzinfo.normalize(dt) 

193 return dt 

194 

195 

196def _get_time( 

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

198 tzinfo: datetime.tzinfo | None = None, 

199) -> datetime.time: 

200 """ 

201 Get a timezoned time from a given instant. 

202 

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

204 

205 :param time: time, datetime or None 

206 :rtype: time 

207 """ 

208 if time is None: 

209 time = datetime.datetime.now(UTC) 

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

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

212 

213 if time.tzinfo is None: 

214 time = time.replace(tzinfo=UTC) 

215 

216 if isinstance(time, datetime.datetime): 

217 if tzinfo is not None: 

218 time = time.astimezone(tzinfo) 

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

220 time = tzinfo.normalize(time) 

221 time = time.timetz() 

222 elif tzinfo is not None: 

223 time = time.replace(tzinfo=tzinfo) 

224 return time 

225 

226 

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

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

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

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

231 the functions of Babel that operate with dates. 

232 

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

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

235 

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

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

238 """ 

239 if zone is None: 

240 return LOCALTZ 

241 if not isinstance(zone, str): 

242 return zone 

243 

244 if pytz: 

245 try: 

246 return pytz.timezone(zone) 

247 except pytz.UnknownTimeZoneError as e: 

248 exc = e 

249 else: 

250 assert zoneinfo 

251 try: 

252 return zoneinfo.ZoneInfo(zone) 

253 except zoneinfo.ZoneInfoNotFoundError as e: 

254 exc = e 

255 

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

257 

258 

259def get_period_names( 

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

261 context: _Context = 'stand-alone', 

262 locale: Locale | str | None = None, 

263) -> LocaleDataDict: 

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

265 

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

267 'AM' 

268 

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

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

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

272 """ 

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

274 

275 

276def get_day_names( 

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

278 context: _Context = 'format', 

279 locale: Locale | str | None = None, 

280) -> LocaleDataDict: 

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

282 

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

284 'Tuesday' 

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

286 'Tu' 

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

288 'mar' 

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

290 'D' 

291 

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

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

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

295 """ 

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

297 

298 

299def get_month_names( 

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

301 context: _Context = 'format', 

302 locale: Locale | str | None = None, 

303) -> LocaleDataDict: 

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

305 

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

307 'January' 

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

309 'ene' 

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

311 'J' 

312 

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

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

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

316 """ 

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

318 

319 

320def get_quarter_names( 

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

322 context: _Context = 'format', 

323 locale: Locale | str | None = None, 

324) -> LocaleDataDict: 

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

326 

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

328 '1st quarter' 

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

330 'Q1' 

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

332 '1' 

333 

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

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

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

337 """ 

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

339 

340 

341def get_era_names( 

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

343 locale: Locale | str | None = None, 

344) -> LocaleDataDict: 

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

346 

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

348 'Anno Domini' 

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

350 'n. Chr.' 

351 

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

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

354 """ 

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

356 

357 

358def get_relative_name( 

359 field: str, 

360 offset: int, 

361 length: Literal['long', 'short', 'narrow'] = 'long', 

362 locale: Locale | str | None = None, 

363) -> str | None: 

364 """Return the locale's literal name for a relative offset of a date field. 

365 

366 These are the CLDR ``<relative>`` strings such as "yesterday", "today", 

367 "tomorrow" for ``field='day'``, or "last Monday"/"this Monday"/"next Monday" 

368 for ``field='mon'``. 

369 

370 Returns ``None`` when the locale does not define a literal name for the 

371 requested field/offset (in which case a phrase like "in 2 days" built 

372 via :func:`format_timedelta` is typically the right fallback). 

373 

374 >>> get_relative_name('day', -1, locale='en') 

375 'yesterday' 

376 >>> get_relative_name('day', 1, locale='en') 

377 'tomorrow' 

378 >>> get_relative_name('day', -2, locale='de') 

379 'vorgestern' 

380 >>> get_relative_name('year', 1, locale='en') 

381 'next year' 

382 >>> get_relative_name('year', 1, length='short', locale='en') 

383 'next yr.' 

384 >>> get_relative_name('mon', 1, locale='en') 

385 'next Monday' 

386 

387 Offsets without a literal name return ``None``: 

388 

389 >>> get_relative_name('day', 5, locale='en') is None 

390 True 

391 

392 :param field: the date field, e.g. "day", "week", "month", "year", 

393 "hour", "minute", "second", "quarter", or a weekday 

394 abbreviation like "mon", "tue", etc. 

395 :param offset: the offset from the current value of *field*. 

396 :param length: "long", "short", or "narrow". 

397 :param locale: a `Locale` object or a locale identifier. Defaults to the 

398 system time locale. 

399 

400 .. versionadded:: 2.19 

401 """ 

402 locale = Locale.parse(locale or LC_TIME) 

403 date_fields = locale._data['date_fields'] 

404 key = field if length == 'long' else f'{field}-{length}' 

405 data = date_fields.get(key) or date_fields.get(field) or {} 

406 return data.get('relative', {}).get(offset) 

407 

408 

409def get_date_format( 

410 format: _PredefinedTimeFormat = 'medium', 

411 locale: Locale | str | None = None, 

412) -> DateTimePattern: 

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

414 format. 

415 

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

417 <DateTimePattern 'MMM d, y'> 

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

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

420 

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

422 "short" 

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

424 """ 

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

426 

427 

428def get_datetime_format( 

429 format: _PredefinedTimeFormat = 'medium', 

430 locale: Locale | str | None = None, 

431) -> DateTimePattern: 

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

433 specified format. 

434 

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

436 '{1}, {0}' 

437 

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

439 "short" 

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

441 """ 

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

443 if format not in patterns: 

444 format = None 

445 return patterns[format] 

446 

447 

448def get_time_format( 

449 format: _PredefinedTimeFormat = 'medium', 

450 locale: Locale | str | None = None, 

451) -> DateTimePattern: 

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

453 format. 

454 

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

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

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

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

459 

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

461 "short" 

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

463 """ 

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

465 

466 

467def get_timezone_gmt( 

468 datetime: _Instant = None, 

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

470 locale: Locale | str | None = None, 

471 return_z: bool = False, 

472 # TODO(3.0): Make return_z kw-only too 

473 *, 

474 utc: bool = False, 

475) -> str: 

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

477 as string indicating the offset from GMT. 

478 

479 >>> from datetime import datetime 

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

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

482 'GMT+00:00' 

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

484 'Z' 

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

486 '+00' 

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

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

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

490 'GMT-07:00' 

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

492 '-0700' 

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

494 '-07' 

495 

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

497 UTC string is used instead of GMT: 

498 

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

500 'UTC-07:00' 

501 

502 Passing ``utc=True`` requests the localized equivalent of "UTC" instead of 

503 "GMT": 

504 

505 >>> get_timezone_gmt(dt, locale='en', utc=True) 

506 'UTC-07:00' 

507 >>> get_timezone_gmt(dt, locale='ar', utc=True) 

508 'التوقيت العالمي -07:00' 

509 

510 .. versionadded:: 0.9 

511 

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

513 time in UTC is used 

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

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

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

517 when local time offset is 0 

518 :param utc: If ``True``, use the localized equivalent of "UTC" instead of 

519 "GMT" in the long format. Only affects ``width='long'`` as 

520 other widths have no GMT/UTC prefix. 

521 

522 .. versionadded:: 2.19 

523 """ 

524 datetime = _ensure_datetime_tzinfo(_get_datetime(datetime)) 

525 locale = Locale.parse(locale or LC_TIME) 

526 

527 offset = datetime.tzinfo.utcoffset(datetime) 

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

529 if return_z and seconds == 0: 

530 return 'Z' 

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

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

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

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

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

536 pattern = '%s%02d%02d' 

537 elif width == 'iso8601': 

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

539 else: 

540 pattern = locale.zone_formats['gmt_utc' if utc else 'gmt'] % '%s%02d:%02d' 

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

542 

543 

544def get_timezone_location( 

545 dt_or_tzinfo: _DtOrTzinfo = None, 

546 locale: Locale | str | None = None, 

547 return_city: bool = False, 

548) -> str: 

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

550 

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

552 city associated with the time zone: 

553 

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

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

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

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

558 Canada (St. John’s) Time 

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

560 St. John’s 

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

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

563 'Mexiko (Mexiko-Stadt) (Ortszeit)' 

564 

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

566 timezone, just the localized country name is returned: 

567 

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

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

570 'Mitteleuropäische Zeit' 

571 

572 .. versionadded:: 0.9 

573 

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

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

576 UTC is assumed 

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

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

579 for the time zone 

580 :return: the localized timezone name using location format 

581 

582 """ 

583 locale = Locale.parse(locale or LC_TIME) 

584 

585 zone = _get_tz_name(dt_or_tzinfo) 

586 

587 # Get the canonical time-zone code 

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

589 

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

591 

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

593 # localized country name 

594 region_format = locale.zone_formats['region'] 

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

596 if territory not in locale.territories: 

597 territory = 'ZZ' # invalid/unknown 

598 territory_name = locale.territories.get(territory) 

599 if ( 

600 not return_city 

601 and territory_name 

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

603 ): 

604 return region_format % territory_name 

605 

606 # Otherwise, include the city in the output 

607 fallback_format = locale.zone_formats['fallback'] 

608 if 'city' in info: 

609 city_name = info['city'] 

610 else: 

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

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

613 if 'city' in metazone_info: 

614 city_name = metazone_info['city'] 

615 else: 

616 city_name = zone.rpartition('/')[2].replace('_', ' ') 

617 

618 if return_city: 

619 return city_name 

620 if territory_name is None: 

621 # The locale has no name for the zone's region, not even for the 

622 # "unknown region" fallback, so the region is left out. 

623 return region_format % city_name 

624 return region_format % ( 

625 fallback_format 

626 % { 

627 '0': city_name, 

628 '1': territory_name, 

629 } 

630 ) 

631 

632 

633def get_timezone_name( 

634 dt_or_tzinfo: _DtOrTzinfo = None, 

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

636 uncommon: bool = False, 

637 locale: Locale | str | None = None, 

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

639 return_zone: bool = False, 

640) -> str: 

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

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

643 

644 >>> from datetime import time 

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

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

647 'Pacific Standard Time' 

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

649 'America/Los_Angeles' 

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

651 'PST' 

652 

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

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

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

656 time of events that recur across DST changes: 

657 

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

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

660 'Pacific Time' 

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

662 'PT' 

663 

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

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

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

667 

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

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

670 'Mitteleuropäische Zeit' 

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

672 'Horário da Europa Central' 

673 

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

675 included in the representation: 

676 

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

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

679 'Neufundland-Zeit' 

680 

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

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

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

684 format. 

685 

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

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

688 

689 .. versionadded:: 0.9 

690 

691 .. versionchanged:: 1.0 

692 Added `zone_variant` support. 

693 

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

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

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

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

698 current date in UTC is assumed 

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

700 :param uncommon: deprecated and ignored 

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

702 variation is defined from the datetime object 

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

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

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

706 ``'standard'``. 

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

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

709 returns long time zone ID 

710 """ 

711 dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) 

712 locale = Locale.parse(locale or LC_TIME) 

713 

714 zone = _get_tz_name(dt_or_tzinfo) 

715 

716 if zone_variant is None: 

717 if dt is None: 

718 zone_variant = 'generic' 

719 else: 

720 dst = tzinfo.dst(dt) 

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

722 else: 

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

724 raise ValueError('Invalid zone variation') 

725 

726 # Get the canonical time-zone code 

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

728 if return_zone: 

729 return zone 

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

731 # Try explicitly translated zone names first 

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

733 value = info[width][zone_variant] 

734 if value != NO_INHERITANCE_MARKER: 

735 return value 

736 

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

738 if metazone: 

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

740 if width in metazone_info: 

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

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

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

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

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

746 if name and name != NO_INHERITANCE_MARKER: 

747 return name 

748 

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

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

751 if dt is not None: 

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

753 

754 return get_timezone_location(dt_or_tzinfo, locale=locale) 

755 

756 

757def format_date( 

758 date: datetime.date | None = None, 

759 format: _PredefinedTimeFormat | str = 'medium', 

760 locale: Locale | str | None = None, 

761) -> str: 

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

763 

764 >>> from datetime import date 

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

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

767 'Apr 1, 2007' 

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

769 'Sonntag, 1. April 2007' 

770 

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

772 custom date pattern: 

773 

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

775 "Sun, Apr 1, '07" 

776 

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

778 date is used 

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

780 date/time pattern 

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

782 """ 

783 if date is None: 

784 date = datetime.date.today() 

785 elif isinstance(date, datetime.datetime): 

786 date = date.date() 

787 

788 locale = Locale.parse(locale or LC_TIME) 

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

790 format = get_date_format(format, locale=locale) 

791 pattern = parse_pattern(format) 

792 return pattern.apply(date, locale) 

793 

794 

795def format_datetime( 

796 datetime: _Instant = None, 

797 format: _PredefinedTimeFormat | str = 'medium', 

798 tzinfo: datetime.tzinfo | None = None, 

799 locale: Locale | str | None = None, 

800) -> str: 

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

802 

803 >>> from datetime import datetime 

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

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

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

807 

808 For any pattern requiring the display of the timezone: 

809 

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

811 ... locale='fr_FR') 

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

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

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

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

816 

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

818 time is used 

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

820 date/time pattern 

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

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

823 """ 

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

825 

826 locale = Locale.parse(locale or LC_TIME) 

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

828 return ( 

829 get_datetime_format(format, locale=locale) 

830 .replace("'", "") 

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

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

833 ) 

834 else: 

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

836 

837 

838def format_relative_datetime( 

839 relative_date: str, 

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

841 format: _PredefinedTimeFormat = 'medium', 

842 tzinfo: datetime.tzinfo | None = None, 

843 locale: Locale | str | None = None, 

844) -> str: 

845 """Combine a pre-formatted relative date with a time using the locale's 

846 relative datetime pattern. 

847 

848 For instance, in English you might say "tomorrow at 3:30 PM" instead of 

849 "tomorrow, 3:30 PM". 

850 

851 The caller supplies the relative date portion as a string 

852 (such as "tomorrow" or "in 3 days"); this function formats the time and joins the two. 

853 

854 >>> from datetime import time 

855 >>> format_relative_datetime('tomorrow', time(15, 30), locale='en', format='long') 

856 'tomorrow at 3:30:00\u202fPM UTC' 

857 >>> format_relative_datetime('morgen', time(15, 30), locale='de', format='long') 

858 'morgen um 15:30:00 UTC' 

859 

860 When the locale does not define a relative pattern for the requested 

861 width, its standard datetime pattern is used, as CLDR's root locale 

862 aliases ``relative`` patterns to ``standard`` ones: 

863 

864 >>> format_relative_datetime('zítra', time(15, 30), locale='cs', format='short') 

865 'zítra 15:30' 

866 

867 :param relative_date: the already-localized relative date string. 

868 :param time: the time to combine with the relative date. 

869 If ``None`` the current time is used. 

870 :param format: one of "full", "long", "medium", or "short". 

871 :param tzinfo: the timezone to apply to the time for display. 

872 :param locale: a `Locale` object or a locale identifier. 

873 Defaults to the system time locale. 

874 

875 .. versionadded:: 2.19 

876 """ 

877 locale = Locale.parse(locale or LC_TIME) 

878 pattern = ( 

879 locale.datetime_formats_relative.get(format) 

880 or get_datetime_format(format, locale=locale) 

881 ) 

882 return ( 

883 pattern 

884 .replace("'", "") 

885 .replace('{0}', format_time(time, format, tzinfo=tzinfo, locale=locale)) 

886 .replace('{1}', relative_date) 

887 ) 

888 

889 

890def format_time( 

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

892 format: _PredefinedTimeFormat | str = 'medium', 

893 tzinfo: datetime.tzinfo | None = None, 

894 locale: Locale | str | None = None, 

895) -> str: 

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

897 

898 >>> from datetime import datetime, time 

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

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

901 '3:30:00\u202fPM' 

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

903 '15:30' 

904 

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

906 custom time pattern: 

907 

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

909 "03 o'clock PM" 

910 

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

912 timezone has to be specified explicitly: 

913 

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

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

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

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

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

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

920 ... locale='en') 

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

922 

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

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

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

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

927 it is assumed to be in UTC. 

928 

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

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

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

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

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

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

935 

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

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

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

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

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

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

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

943 

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

945 time in UTC is used 

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

947 date/time pattern 

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

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

950 """ 

951 

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

953 # in the pattern 

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

955 

956 time = _get_time(time, tzinfo) 

957 

958 locale = Locale.parse(locale or LC_TIME) 

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

960 format = get_time_format(format, locale=locale) 

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

962 

963 

964def format_skeleton( 

965 skeleton: str, 

966 datetime: _Instant = None, 

967 tzinfo: datetime.tzinfo | None = None, 

968 fuzzy: bool = True, 

969 locale: Locale | str | None = None, 

970) -> str: 

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

972 

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

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

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

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

977 

978 >>> from datetime import datetime 

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

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

981 'dim. 1 avr.' 

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

983 'Sun, Apr 1' 

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

985 '1.4.2007' 

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

987 Traceback (most recent call last): 

988 ... 

989 KeyError: yMMd 

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

991 Traceback (most recent call last): 

992 ... 

993 KeyError: 'GH' 

994 

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

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

997 

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

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

1000 time in UTC is used 

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

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

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

1004 with the requested skeleton is thrown. 

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

1006 """ 

1007 locale = Locale.parse(locale or LC_TIME) 

1008 if fuzzy and skeleton not in locale.datetime_skeletons: 

1009 skeleton = match_skeleton(skeleton, locale.datetime_skeletons) or skeleton 

1010 format = locale.datetime_skeletons[skeleton] 

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

1012 

1013 

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

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

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

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

1018 ('day', 3600 * 24), 

1019 ('hour', 3600), 

1020 ('minute', 60), 

1021 ('second', 1), 

1022) 

1023 

1024 

1025def format_timedelta( 

1026 delta: datetime.timedelta | int, 

1027 granularity: Literal[ 

1028 'year', 

1029 'month', 

1030 'week', 

1031 'day', 

1032 'hour', 

1033 'minute', 

1034 'second', 

1035 ] = 'second', 

1036 threshold: float = 0.85, 

1037 add_direction: bool = False, 

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

1039 locale: Locale | str | None = None, 

1040) -> str: 

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

1042 

1043 >>> from datetime import timedelta 

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

1045 '3 months' 

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

1047 '1 segundo' 

1048 

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

1050 presented, which defaults to a second. 

1051 

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

1053 '1 day' 

1054 

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

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

1057 means the presentation will switch later. For example: 

1058 

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

1060 '1 day' 

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

1062 '23 hours' 

1063 

1064 In addition directional information can be provided that informs 

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

1066 

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

1068 'in 1 hour' 

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

1070 '1 hour ago' 

1071 

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

1073 

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

1075 '3 hr' 

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

1077 '3h' 

1078 

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

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

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

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

1083 "hour", "minute" or "second" 

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

1085 switches to the next higher unit 

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

1087 include directional information. For instance a 

1088 positive timedelta will include the information about 

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

1090 about the value being in the past. 

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

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

1093 maintain compatibility) 

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

1095 """ 

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

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

1098 if format == 'medium': 

1099 warnings.warn( 

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

1101 category=DeprecationWarning, 

1102 stacklevel=2, 

1103 ) 

1104 format = 'long' 

1105 if isinstance(delta, datetime.timedelta): 

1106 seconds = delta.total_seconds() 

1107 else: 

1108 seconds = delta 

1109 locale = Locale.parse(locale or LC_TIME) 

1110 date_fields = locale._data["date_fields"] 

1111 unit_patterns = locale._data["unit_patterns"] 

1112 

1113 def _iter_patterns(a_unit): 

1114 if add_direction: 

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

1116 # before falling back to the default. 

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

1118 if seconds >= 0: 

1119 yield unit_rel_patterns['future'] 

1120 else: 

1121 yield unit_rel_patterns['past'] 

1122 a_unit = f"duration-{a_unit}" 

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

1124 

1125 for unit, secs_per_unit in TIMEDELTA_UNITS: 

1126 value = abs(seconds) / secs_per_unit 

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

1128 if unit == granularity and value > 0: 

1129 value = max(1, value) 

1130 value = int(round(value)) 

1131 plural_form = locale.plural_form(value) 

1132 pattern = None 

1133 for patterns in _iter_patterns(unit): 

1134 if patterns is not None: 

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

1136 if pattern: 

1137 break 

1138 # This really should not happen 

1139 if pattern is None: 

1140 return '' 

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

1142 

1143 return '' 

1144 

1145 

1146def _format_fallback_interval( 

1147 start: _Instant, 

1148 end: _Instant, 

1149 skeleton: str | None, 

1150 tzinfo: datetime.tzinfo | None, 

1151 locale: Locale, 

1152) -> str: 

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

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

1155 elif all( 

1156 # Both are just dates 

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

1158 for d in (start, end) 

1159 ): 

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

1161 elif all( 

1162 # Both are times 

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

1164 for d in (start, end) 

1165 ): 

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

1167 else: 

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

1169 

1170 formatted_start = format(start) 

1171 formatted_end = format(end) 

1172 

1173 if formatted_start == formatted_end: 

1174 return format(start) 

1175 

1176 return ( 

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

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

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

1180 ) 

1181 

1182 

1183def format_interval( 

1184 start: _Instant, 

1185 end: _Instant, 

1186 skeleton: str | None = None, 

1187 tzinfo: datetime.tzinfo | None = None, 

1188 fuzzy: bool = True, 

1189 locale: Locale | str | None = None, 

1190) -> str: 

1191 """ 

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

1193 

1194 >>> from datetime import date, time 

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

1196 '15.–17.1.2016' 

1197 

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

1199 '12:12–16:16' 

1200 

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

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

1203 

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

1205 '16:18–16:24' 

1206 

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

1208 

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

1210 '16:18' 

1211 

1212 Unknown skeletons fall back to "default" formatting. 

1213 

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

1215 '2015/01/01~2017/01/01' 

1216 

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

1218 '16:18:00~16:24:00' 

1219 

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

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

1222 

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

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

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

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

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

1228 close enough to it. 

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

1230 :return: Formatted interval 

1231 """ 

1232 locale = Locale.parse(locale or LC_TIME) 

1233 

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

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

1236 

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

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

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

1240 

1241 interval_formats = locale.interval_formats 

1242 

1243 if skeleton not in interval_formats or not skeleton: 

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

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

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

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

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

1249 if skeleton and fuzzy: 

1250 skeleton = match_skeleton(skeleton, interval_formats) 

1251 else: 

1252 skeleton = None 

1253 if not skeleton: # Still no match whatsoever? 

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

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

1256 

1257 skel_formats = interval_formats[skeleton] 

1258 

1259 if start == end: 

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

1261 

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

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

1264 

1265 start_fmt = DateTimeFormat(start, locale=locale) 

1266 end_fmt = DateTimeFormat(end, locale=locale) 

1267 

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

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

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

1271 # > single date using availableFormats, and return. 

1272 

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

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

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

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

1277 return "".join( 

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

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

1280 ) 

1281 

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

1283 

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

1285 

1286 

1287def get_period_id( 

1288 time: _Instant, 

1289 tzinfo: datetime.tzinfo | None = None, 

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

1291 locale: Locale | str | None = None, 

1292) -> str: 

1293 """ 

1294 Get the day period ID for a given time. 

1295 

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

1297 

1298 >>> from datetime import time 

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

1300 'Morgen' 

1301 

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

1303 'midnight' 

1304 

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

1306 'morning1' 

1307 

1308 :param time: The time to inspect. 

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

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

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

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

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

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

1315 """ 

1316 time = _get_time(time, tzinfo) 

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

1318 locale = Locale.parse(locale or LC_TIME) 

1319 

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

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

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

1323 

1324 for rule_id, rules in rulesets: 

1325 for rule in rules: 

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

1327 return rule_id 

1328 

1329 for rule_id, rules in rulesets: 

1330 for rule in rules: 

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

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

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

1334 return rule_id 

1335 else: 

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

1337 if ( 

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

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

1340 ): 

1341 return rule_id 

1342 

1343 start_ok = end_ok = False 

1344 

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

1346 start_ok = True 

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

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

1349 # excuse the lack of test coverage. 

1350 end_ok = True 

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

1352 end_ok = True 

1353 if "after" in rule: 

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

1355 

1356 if start_ok and end_ok: 

1357 return rule_id 

1358 

1359 if seconds_past_midnight < 43200: 

1360 return "am" 

1361 else: 

1362 return "pm" 

1363 

1364 

1365class ParseError(ValueError): 

1366 pass 

1367 

1368 

1369def parse_date( 

1370 string: str, 

1371 locale: Locale | str | None = None, 

1372 format: _PredefinedTimeFormat | str = 'medium', 

1373) -> datetime.date: 

1374 """Parse a date from a string. 

1375 

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

1377 

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

1379 datetime.date(2004, 4, 1) 

1380 

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

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

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

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

1385 the string. 

1386 

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

1388 datetime.date(2004, 4, 1) 

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

1390 datetime.date(2004, 4, 1) 

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

1392 datetime.date(2004, 4, 1) 

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

1394 datetime.date(2004, 4, 1) 

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

1396 datetime.date(2004, 4, 1) 

1397 

1398 :param string: the string containing the date 

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

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

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

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

1403 (see ``get_time_format``) 

1404 """ 

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

1406 if not numbers: 

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

1408 

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

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

1411 # extended YYYY-MM-DD or basic YYYYMMDD 

1412 iso_alike = re.match( 

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

1414 string, 

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

1416 ) 

1417 if iso_alike and use_predefined_format: 

1418 try: 

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

1420 except ValueError: 

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

1422 

1423 if use_predefined_format: 

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

1425 else: 

1426 fmt = parse_pattern(format) 

1427 format_str = fmt.pattern.lower() 

1428 year_idx = format_str.index('y') 

1429 month_idx = format_str.find('m') 

1430 if month_idx < 0: 

1431 month_idx = format_str.index('l') 

1432 day_idx = format_str.index('d') 

1433 

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

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

1436 

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

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

1439 

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

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

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

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

1444 if month > 12: 

1445 month, day = day, month 

1446 try: 

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

1448 except ValueError as e: 

1449 raise ParseError( 

1450 f"'{string}' does not represent a valid date for format '{fmt.pattern}'", 

1451 ) from e 

1452 

1453 

1454def parse_time( 

1455 string: str, 

1456 locale: Locale | str | None = None, 

1457 format: _PredefinedTimeFormat | str = 'medium', 

1458) -> datetime.time: 

1459 """Parse a time from a string. 

1460 

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

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

1463 

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

1465 the time instead. 

1466 

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

1468 datetime.time(15, 30) 

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

1470 datetime.time(15, 30) 

1471 

1472 :param string: the string containing the time 

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

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

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

1476 (see ``get_time_format``) 

1477 :return: the parsed time 

1478 :rtype: `time` 

1479 """ 

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

1481 if not numbers: 

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

1483 

1484 # TODO: try ISO format first? 

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

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

1487 else: 

1488 fmt = parse_pattern(format) 

1489 format_str = fmt.pattern.lower() 

1490 hour_idx = format_str.find('h') 

1491 if hour_idx < 0: 

1492 hour_idx = format_str.index('k') 

1493 min_idx = format_str.index('m') 

1494 # format might not contain seconds 

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

1496 sec_idx = math.inf 

1497 

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

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

1500 

1501 # TODO: support time zones 

1502 

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

1504 # if it does, convert the hour from the 12-hour clock, on which 

1505 # 12 AM and 12 PM are 00:00 and 12:00 respectively. 

1506 hour = int(numbers[indexes['H']]) 

1507 if 'a' in format_str: 

1508 if 'pm' in string.lower(): 

1509 if hour != 12: 

1510 hour += 12 

1511 elif 'am' in string.lower() and hour == 12: 

1512 hour = 0 

1513 

1514 # Parse up to three numbers from the string. 

1515 minute = second = 0 

1516 if len(numbers) > 1: 

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

1518 if len(numbers) > 2: 

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

1520 try: 

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

1522 except ValueError as e: 

1523 raise ParseError( 

1524 f"'{string}' does not represent a valid time for format '{fmt.pattern}'", 

1525 ) from e 

1526 

1527 

1528class DateTimePattern: 

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

1530 self.pattern = pattern 

1531 self.format = format 

1532 

1533 def __repr__(self) -> str: 

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

1535 

1536 def __str__(self) -> str: 

1537 pat = self.pattern 

1538 return pat 

1539 

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

1541 if not isinstance(other, DateTimeFormat): 

1542 return NotImplemented 

1543 return self.format % other 

1544 

1545 def apply( 

1546 self, 

1547 datetime: datetime.date | datetime.time, 

1548 locale: Locale | str | None, 

1549 reference_date: datetime.date | None = None, 

1550 ) -> str: 

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

1552 

1553 

1554class DateTimeFormat: 

1555 def __init__( 

1556 self, 

1557 value: datetime.date | datetime.time, 

1558 locale: Locale | str, 

1559 reference_date: datetime.date | None = None, 

1560 ) -> None: 

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

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

1563 value = value.replace(tzinfo=UTC) 

1564 self.value = value 

1565 self.locale = Locale.parse(locale) 

1566 self.reference_date = reference_date 

1567 

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

1569 char = name[0] 

1570 num = len(name) 

1571 if char == 'G': 

1572 return self.format_era(char, num) 

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

1574 return self.format_year(char, num) 

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

1576 return self.format_quarter(char, num) 

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

1578 return self.format_month(char, num) 

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

1580 return self.format_week(char, num) 

1581 elif char == 'd': 

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

1583 elif char == 'D': 

1584 return self.format_day_of_year(num) 

1585 elif char == 'F': 

1586 return self.format_day_of_week_in_month() 

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

1588 return self.format_weekday(char, num) 

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

1590 return self.format_period(char, num) 

1591 elif char == 'h': 

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

1593 return self.format(12, num) 

1594 else: 

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

1596 elif char == 'H': 

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

1598 elif char == 'K': 

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

1600 elif char == 'k': 

1601 if self.value.hour == 0: 

1602 return self.format(24, num) 

1603 else: 

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

1605 elif char == 'm': 

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

1607 elif char == 's': 

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

1609 elif char == 'S': 

1610 return self.format_frac_seconds(num) 

1611 elif char == 'A': 

1612 return self.format_milliseconds_in_day(num) 

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

1614 return self.format_timezone(char, num) 

1615 else: 

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

1617 

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

1619 char = str(char)[0] 

1620 if char == 'y': 

1621 return self.value.year 

1622 elif char == 'M': 

1623 return self.value.month 

1624 elif char == 'd': 

1625 return self.value.day 

1626 elif char == 'H': 

1627 return self.value.hour 

1628 elif char == 'h': 

1629 return self.value.hour % 12 or 12 

1630 elif char == 'm': 

1631 return self.value.minute 

1632 elif char == 'a': 

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

1634 else: 

1635 raise NotImplementedError( 

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

1637 ) 

1638 

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

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

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

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

1643 

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

1645 value = self.value.year 

1646 if char.isupper(): 

1647 month = self.value.month 

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

1649 value -= 1 

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

1651 value += 1 

1652 year = self.format(value, num) 

1653 if num == 2: 

1654 year = year[-2:] 

1655 return year 

1656 

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

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

1659 if num <= 2: 

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

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

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

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

1664 

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

1666 if num <= 2: 

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

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

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

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

1671 

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

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

1674 week = self.get_week_of_year() 

1675 return self.format(week, num) 

1676 else: # week of month 

1677 week = self.get_week_of_month() 

1678 return str(week) 

1679 

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

1681 """ 

1682 Return weekday from parsed datetime according to format pattern. 

1683 

1684 >>> from datetime import date 

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

1686 >>> format.format_weekday() 

1687 'Sunday' 

1688 

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

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

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

1692 'Sun' 

1693 

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

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

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

1697 '01' 

1698 

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

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

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

1702 '1' 

1703 

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

1705 :param num: count of format character 

1706 

1707 """ 

1708 if num < 3: 

1709 if char.islower(): 

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

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

1712 num = 3 

1713 weekday = self.value.weekday() 

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

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

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

1717 

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

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

1720 

1721 def format_day_of_week_in_month(self) -> str: 

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

1723 

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

1725 """ 

1726 Return period from parsed datetime according to format pattern. 

1727 

1728 >>> from datetime import datetime, time 

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

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

1731 'ip.' 

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

1733 'iltap.' 

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

1735 'iltapäivä' 

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

1737 'iltapäivällä' 

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

1739 'ip.' 

1740 

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

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

1743 '上午' 

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

1745 '清晨' 

1746 

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

1748 :param num: count of format character 

1749 

1750 """ 

1751 widths = [ 

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

1753 'wide', 

1754 'narrow', 

1755 'abbreviated', 

1756 ] 

1757 if char == 'a': 

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

1759 context = 'format' 

1760 else: 

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

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

1763 for width in widths: 

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

1765 if period in period_names: 

1766 return period_names[period] 

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

1768 

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

1770 """ Return fractional seconds. 

1771 

1772 Truncates the time's microseconds to the number of digits passed in, 

1773 as specified by UTS #35. 

1774 """ 

1775 micro = self.value.microsecond 

1776 frac = micro // 10**(6 - num) if num <= 6 else micro * 10**(num - 6) 

1777 return self.format(frac, num) 

1778 

1779 def format_milliseconds_in_day(self, num): 

1780 msecs = ( 

1781 self.value.microsecond // 1000 

1782 + self.value.second * 1000 

1783 + self.value.minute * 60000 

1784 + self.value.hour * 3600000 

1785 ) 

1786 return self.format(msecs, num) 

1787 

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

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

1790 

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

1792 # reference date which is important to distinguish between timezone 

1793 # variants (summer/standard time) 

1794 value = self.value 

1795 if self.reference_date: 

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

1797 

1798 if char == 'z': 

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

1800 elif char == 'Z': 

1801 if num == 5: 

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

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

1804 elif char == 'O': 

1805 if num == 4: 

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

1807 # TODO: To add support for O:1 

1808 elif char == 'v': 

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

1810 elif char == 'V': 

1811 if num == 1: 

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

1813 elif num == 2: 

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

1815 elif num == 3: 

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

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

1818 elif char in 'Xx': 

1819 return_z = char == 'X' 

1820 if num == 1: 

1821 width = 'iso8601_short' 

1822 elif num in (2, 4): 

1823 width = 'short' 

1824 elif num in (3, 5): 

1825 width = 'iso8601' 

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

1827 

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

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

1830 

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

1832 if date is None: 

1833 date = self.value 

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

1835 

1836 def get_week_of_year(self) -> int: 

1837 """Return the week of the year.""" 

1838 day_of_year = self.get_day_of_year(self.value) 

1839 week = self.get_week_number(day_of_year) 

1840 if week == 0: 

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

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

1843 elif week > 52: 

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

1845 if ( 

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

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

1848 ): 

1849 week = 1 

1850 return week 

1851 

1852 def get_week_of_month(self) -> int: 

1853 """Return the week of the month.""" 

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

1855 

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

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

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

1859 

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

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

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

1863 

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

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

1866 1 

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

1868 2 

1869 

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

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

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

1873 current date is assumed 

1874 """ 

1875 if day_of_week is None: 

1876 day_of_week = self.value.weekday() 

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

1878 if first_day < 0: 

1879 first_day += 7 

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

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

1882 week_number += 1 

1883 return week_number 

1884 

1885 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1900} # fmt: skip 

1901 

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

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

1904#: in order of decreasing magnitude. 

1905PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" 

1906 

1907 

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

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

1910 

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

1912 '%(MMMM)s%(d)s' 

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

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

1915 

1916 Pattern can contain literal strings in single quotes: 

1917 

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

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

1920 

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

1922 characters: 

1923 

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

1925 "%(hh)s o'clock" 

1926 

1927 :param pattern: the formatting pattern to parse 

1928 """ 

1929 if isinstance(pattern, DateTimePattern): 

1930 return pattern 

1931 return _cached_parse_pattern(pattern) 

1932 

1933 

1934@lru_cache(maxsize=1024) 

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

1936 result = [] 

1937 

1938 for tok_type, tok_value in tokenize_pattern(pattern): 

1939 if tok_type == "chars": 

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

1941 elif tok_type == "field": 

1942 fieldchar, fieldnum = tok_value 

1943 limit = PATTERN_CHARS[fieldchar] 

1944 if limit and fieldnum not in limit: 

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

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

1947 else: 

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

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

1950 

1951 

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

1953 """ 

1954 Tokenize date format patterns. 

1955 

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

1957 

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

1959 

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

1961 

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

1963 

1964 :param pattern: Pattern string 

1965 """ 

1966 result = [] 

1967 quotebuf = None 

1968 charbuf = [] 

1969 fieldchar = [''] 

1970 fieldnum = [0] 

1971 

1972 def append_chars(): 

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

1974 del charbuf[:] 

1975 

1976 def append_field(): 

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

1978 fieldchar[0] = '' 

1979 fieldnum[0] = 0 

1980 

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

1982 if quotebuf is None: 

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

1984 if fieldchar[0]: 

1985 append_field() 

1986 elif charbuf: 

1987 append_chars() 

1988 quotebuf = [] 

1989 elif char in PATTERN_CHARS: 

1990 if charbuf: 

1991 append_chars() 

1992 if char == fieldchar[0]: 

1993 fieldnum[0] += 1 

1994 else: 

1995 if fieldchar[0]: 

1996 append_field() 

1997 fieldchar[0] = char 

1998 fieldnum[0] = 1 

1999 else: 

2000 if fieldchar[0]: 

2001 append_field() 

2002 charbuf.append(char) 

2003 

2004 elif quotebuf is not None: 

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

2006 charbuf.extend(quotebuf) 

2007 quotebuf = None 

2008 else: # inside quote 

2009 quotebuf.append(char) 

2010 

2011 if fieldchar[0]: 

2012 append_field() 

2013 elif charbuf: 

2014 append_chars() 

2015 

2016 return result 

2017 

2018 

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

2020 """ 

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

2022 

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

2024 """ 

2025 output = [] 

2026 for tok_type, tok_value in tokens: 

2027 if tok_type == "field": 

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

2029 elif tok_type == "chars": 

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

2031 output.append(tok_value) 

2032 else: 

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

2034 return "".join(output) 

2035 

2036 

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

2038 """ 

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

2040 

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

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

2043 

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

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

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

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

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

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

2050 >>> split_interval_pattern("MMM d") 

2051 ['MMM d'] 

2052 >>> split_interval_pattern("y G") 

2053 ['y G'] 

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

2055 ['MMM d – ', 'd'] 

2056 

2057 :param pattern: Interval pattern string 

2058 :return: list of "subpatterns" 

2059 """ 

2060 

2061 seen_fields = set() 

2062 parts = [[]] 

2063 

2064 for tok_type, tok_value in tokenize_pattern(pattern): 

2065 if tok_type == "field": 

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

2067 parts.append([]) 

2068 seen_fields.clear() 

2069 seen_fields.add(tok_value[0]) 

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

2071 

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

2073 

2074 

2075def match_skeleton( 

2076 skeleton: str, 

2077 options: Iterable[str], 

2078 allow_different_fields: bool = False, 

2079) -> str | None: 

2080 """ 

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

2082 

2083 This uses the rules outlined in the TR35 document. 

2084 

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

2086 'yMd' 

2087 

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

2089 'jyMMd' 

2090 

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

2092 

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

2094 'hmv' 

2095 

2096 :param skeleton: The skeleton to match 

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

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

2099 than the skeleton requested. 

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

2101 :rtype: str|None 

2102 """ 

2103 

2104 # TODO: maybe implement pattern expansion? 

2105 

2106 # Based on the implementation in 

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

2108 

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

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

2111 

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

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

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

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

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

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

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

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

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

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

2122 

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

2124 best_skeleton = None 

2125 best_distance = None 

2126 for option in options: 

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

2128 distance = 0 

2129 for field in PATTERN_CHARS: 

2130 input_width = get_input_field_width(field, 0) 

2131 opt_width = get_opt_field_width(field, 0) 

2132 if input_width == opt_width: 

2133 continue 

2134 if opt_width == 0 or input_width == 0: 

2135 if not allow_different_fields: # This one is not okay 

2136 option = None 

2137 break 

2138 # Magic weight constant for "entirely different fields" 

2139 distance += 0x1000 

2140 elif field == 'M' and ( 

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

2142 ): 

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

2144 distance += 0x100 

2145 else: 

2146 distance += abs(input_width - opt_width) 

2147 

2148 if not option: 

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

2150 continue 

2151 

2152 if not best_skeleton or distance < best_distance: 

2153 best_skeleton = option 

2154 best_distance = distance 

2155 

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

2157 break 

2158 

2159 return best_skeleton