Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/babel/dates.py: 12%

701 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:39 +0000

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-2023 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.utcnow()) 

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.utcnow() 

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

152 return datetime.datetime.utcfromtimestamp(instant) 

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.utcnow() 

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

206 time = datetime.datetime.utcfromtimestamp(time) 

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

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

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

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

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('"medium" value for format param of format_timedelta' 

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

927 category=DeprecationWarning) 

928 format = 'long' 

929 if isinstance(delta, datetime.timedelta): 

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

931 else: 

932 seconds = delta 

933 locale = Locale.parse(locale) 

934 

935 def _iter_patterns(a_unit): 

936 if add_direction: 

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

938 if seconds >= 0: 

939 yield unit_rel_patterns['future'] 

940 else: 

941 yield unit_rel_patterns['past'] 

942 a_unit = f"duration-{a_unit}" 

943 yield locale._data['unit_patterns'].get(a_unit, {}).get(format) 

944 

945 for unit, secs_per_unit in TIMEDELTA_UNITS: 

946 value = abs(seconds) / secs_per_unit 

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

948 if unit == granularity and value > 0: 

949 value = max(1, value) 

950 value = int(round(value)) 

951 plural_form = locale.plural_form(value) 

952 pattern = None 

953 for patterns in _iter_patterns(unit): 

954 if patterns is not None: 

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

956 break 

957 # This really should not happen 

958 if pattern is None: 

959 return '' 

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

961 

962 return '' 

963 

964 

965def _format_fallback_interval( 

966 start: _Instant, 

967 end: _Instant, 

968 skeleton: str | None, 

969 tzinfo: datetime.tzinfo | None, 

970 locale: Locale | str | None = LC_TIME, 

971) -> str: 

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

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

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

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

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

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

978 else: 

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

980 

981 formatted_start = format(start) 

982 formatted_end = format(end) 

983 

984 if formatted_start == formatted_end: 

985 return format(start) 

986 

987 return ( 

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

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

990 replace("{1}", formatted_end) 

991 ) 

992 

993 

994def format_interval( 

995 start: _Instant, 

996 end: _Instant, 

997 skeleton: str | None = None, 

998 tzinfo: datetime.tzinfo | None = None, 

999 fuzzy: bool = True, 

1000 locale: Locale | str | None = LC_TIME, 

1001) -> str: 

1002 """ 

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

1004 

1005 >>> from datetime import date, time 

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

1007 u'15.\u201317.1.2016' 

1008 

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

1010 '12:12\u201316:16' 

1011 

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

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

1014 

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

1016 '16:18\u201316:24' 

1017 

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

1019 

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

1021 '16:18' 

1022 

1023 Unknown skeletons fall back to "default" formatting. 

1024 

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

1026 '2015/01/01\uff5e2017/01/01' 

1027 

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

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

1030 

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

1032 '15.01.2016\u2009–\u200917.01.2016' 

1033 

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

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

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

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

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

1039 close enough to it. 

1040 :param locale: A locale object or identifier. 

1041 :return: Formatted interval 

1042 """ 

1043 locale = Locale.parse(locale) 

1044 

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

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

1047 

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

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

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

1051 

1052 interval_formats = locale.interval_formats 

1053 

1054 if skeleton not in interval_formats or not skeleton: 

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

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

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

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

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

1060 if skeleton and fuzzy: 

1061 skeleton = match_skeleton(skeleton, interval_formats) 

1062 else: 

1063 skeleton = None 

1064 if not skeleton: # Still no match whatsoever? 

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

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

1067 

1068 skel_formats = interval_formats[skeleton] 

1069 

1070 if start == end: 

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

1072 

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

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

1075 

1076 start_fmt = DateTimeFormat(start, locale=locale) 

1077 end_fmt = DateTimeFormat(end, locale=locale) 

1078 

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

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

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

1082 # > single date using availableFormats, and return. 

1083 

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

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

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

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

1088 return "".join( 

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

1090 for pattern, instant 

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

1092 ) 

1093 

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

1095 

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

1097 

1098 

1099def get_period_id( 

1100 time: _Instant, 

1101 tzinfo: datetime.tzinfo | None = None, 

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

1103 locale: Locale | str | None = LC_TIME, 

1104) -> str: 

1105 """ 

1106 Get the day period ID for a given time. 

1107 

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

1109 

1110 >>> from datetime import time 

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

1112 u'Morgen' 

1113 

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

1115 u'midnight' 

1116 

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

1118 u'night1' 

1119 

1120 :param time: The time to inspect. 

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

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

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

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

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

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

1127 """ 

1128 time = _get_time(time, tzinfo) 

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

1130 locale = Locale.parse(locale) 

1131 

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

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

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

1135 

1136 for rule_id, rules in rulesets: 

1137 for rule in rules: 

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

1139 return rule_id 

1140 

1141 for rule_id, rules in rulesets: 

1142 for rule in rules: 

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

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

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

1146 return rule_id 

1147 else: 

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

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

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

1151 return rule_id 

1152 

1153 start_ok = end_ok = False 

1154 

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

1156 start_ok = True 

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

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

1159 # excuse the lack of test coverage. 

1160 end_ok = True 

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

1162 end_ok = True 

1163 if "after" in rule: 

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

1165 

1166 if start_ok and end_ok: 

1167 return rule_id 

1168 

1169 if seconds_past_midnight < 43200: 

1170 return "am" 

1171 else: 

1172 return "pm" 

1173 

1174 

1175class ParseError(ValueError): 

1176 pass 

1177 

1178 

1179def parse_date( 

1180 string: str, 

1181 locale: Locale | str | None = LC_TIME, 

1182 format: _PredefinedTimeFormat = 'medium', 

1183) -> datetime.date: 

1184 """Parse a date from a string. 

1185 

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

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

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

1189 

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

1191 datetime.date(2004, 4, 1) 

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

1193 datetime.date(2004, 4, 1) 

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

1195 datetime.date(2004, 4, 1) 

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

1197 datetime.date(2004, 4, 1) 

1198 

1199 :param string: the string containing the date 

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

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

1202 """ 

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

1204 if not numbers: 

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

1206 

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

1208 # extended YYYY-MM-DD or basic YYYYMMDD 

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

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

1211 if iso_alike: 

1212 try: 

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

1214 except ValueError: 

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

1216 

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

1218 year_idx = format_str.index('y') 

1219 month_idx = format_str.index('m') 

1220 if month_idx < 0: 

1221 month_idx = format_str.index('l') 

1222 day_idx = format_str.index('d') 

1223 

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

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

1226 

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

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

1229 

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

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

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

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

1234 if month > 12: 

1235 month, day = day, month 

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

1237 

1238 

1239def parse_time( 

1240 string: str, 

1241 locale: Locale | str | None = LC_TIME, 

1242 format: _PredefinedTimeFormat = 'medium', 

1243) -> datetime.time: 

1244 """Parse a time from a string. 

1245 

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

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

1248 

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

1250 datetime.time(15, 30) 

1251 

1252 :param string: the string containing the time 

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

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

1255 :return: the parsed time 

1256 :rtype: `time` 

1257 """ 

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

1259 if not numbers: 

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

1261 

1262 # TODO: try ISO format first? 

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

1264 hour_idx = format_str.index('h') 

1265 if hour_idx < 0: 

1266 hour_idx = format_str.index('k') 

1267 min_idx = format_str.index('m') 

1268 sec_idx = format_str.index('s') 

1269 

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

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

1272 

1273 # TODO: support time zones 

1274 

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

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

1277 hour_offset = 0 

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

1279 hour_offset = 12 

1280 

1281 # Parse up to three numbers from the string. 

1282 minute = second = 0 

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

1284 if len(numbers) > 1: 

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

1286 if len(numbers) > 2: 

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

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

1289 

1290 

1291class DateTimePattern: 

1292 

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

1294 self.pattern = pattern 

1295 self.format = format 

1296 

1297 def __repr__(self) -> str: 

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

1299 

1300 def __str__(self) -> str: 

1301 pat = self.pattern 

1302 return pat 

1303 

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

1305 if not isinstance(other, DateTimeFormat): 

1306 return NotImplemented 

1307 return self.format % other 

1308 

1309 def apply( 

1310 self, 

1311 datetime: datetime.date | datetime.time, 

1312 locale: Locale | str | None, 

1313 reference_date: datetime.date | None = None, 

1314 ) -> str: 

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

1316 

1317 

1318class DateTimeFormat: 

1319 

1320 def __init__( 

1321 self, 

1322 value: datetime.date | datetime.time, 

1323 locale: Locale | str, 

1324 reference_date: datetime.date | None = None 

1325 ) -> None: 

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

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

1328 value = value.replace(tzinfo=UTC) 

1329 self.value = value 

1330 self.locale = Locale.parse(locale) 

1331 self.reference_date = reference_date 

1332 

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

1334 char = name[0] 

1335 num = len(name) 

1336 if char == 'G': 

1337 return self.format_era(char, num) 

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

1339 return self.format_year(char, num) 

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

1341 return self.format_quarter(char, num) 

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

1343 return self.format_month(char, num) 

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

1345 return self.format_week(char, num) 

1346 elif char == 'd': 

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

1348 elif char == 'D': 

1349 return self.format_day_of_year(num) 

1350 elif char == 'F': 

1351 return self.format_day_of_week_in_month() 

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

1353 return self.format_weekday(char, num) 

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

1355 return self.format_period(char, num) 

1356 elif char == 'h': 

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

1358 return self.format(12, num) 

1359 else: 

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

1361 elif char == 'H': 

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

1363 elif char == 'K': 

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

1365 elif char == 'k': 

1366 if self.value.hour == 0: 

1367 return self.format(24, num) 

1368 else: 

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

1370 elif char == 'm': 

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

1372 elif char == 's': 

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

1374 elif char == 'S': 

1375 return self.format_frac_seconds(num) 

1376 elif char == 'A': 

1377 return self.format_milliseconds_in_day(num) 

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

1379 return self.format_timezone(char, num) 

1380 else: 

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

1382 

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

1384 char = str(char)[0] 

1385 if char == 'y': 

1386 return self.value.year 

1387 elif char == 'M': 

1388 return self.value.month 

1389 elif char == 'd': 

1390 return self.value.day 

1391 elif char == 'H': 

1392 return self.value.hour 

1393 elif char == 'h': 

1394 return self.value.hour % 12 or 12 

1395 elif char == 'm': 

1396 return self.value.minute 

1397 elif char == 'a': 

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

1399 else: 

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

1401 

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

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

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

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

1406 

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

1408 value = self.value.year 

1409 if char.isupper(): 

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

1411 year = self.format(value, num) 

1412 if num == 2: 

1413 year = year[-2:] 

1414 return year 

1415 

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

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

1418 if num <= 2: 

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

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

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

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

1423 

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

1425 if num <= 2: 

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

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

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

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

1430 

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

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

1433 day_of_year = self.get_day_of_year() 

1434 week = self.get_week_number(day_of_year) 

1435 if week == 0: 

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

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

1438 date.weekday()) 

1439 return self.format(week, num) 

1440 else: # week of month 

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

1442 if week == 0: 

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

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

1445 return str(week) 

1446 

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

1448 """ 

1449 Return weekday from parsed datetime according to format pattern. 

1450 

1451 >>> from datetime import date 

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

1453 >>> format.format_weekday() 

1454 u'Sunday' 

1455 

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

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

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

1459 u'Sun' 

1460 

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

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

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

1464 '01' 

1465 

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

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

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

1469 '1' 

1470 

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

1472 :param num: count of format character 

1473 

1474 """ 

1475 if num < 3: 

1476 if char.islower(): 

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

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

1479 num = 3 

1480 weekday = self.value.weekday() 

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

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

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

1484 

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

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

1487 

1488 def format_day_of_week_in_month(self) -> str: 

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

1490 

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

1492 """ 

1493 Return period from parsed datetime according to format pattern. 

1494 

1495 >>> from datetime import datetime, time 

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

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

1498 u'ip.' 

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

1500 u'iltap.' 

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

1502 u'iltapäivä' 

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

1504 u'iltapäivällä' 

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

1506 u'ip.' 

1507 

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

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

1510 u'上午' 

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

1512 u'清晨' 

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

1514 u'清晨' 

1515 

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

1517 :param num: count of format character 

1518 

1519 """ 

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

1521 'wide', 'narrow', 'abbreviated'] 

1522 if char == 'a': 

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

1524 context = 'format' 

1525 else: 

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

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

1528 for width in widths: 

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

1530 if period in period_names: 

1531 return period_names[period] 

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

1533 

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

1535 """ Return fractional seconds. 

1536 

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

1538 of digits passed in. 

1539 """ 

1540 value = self.value.microsecond / 1000000 

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

1542 

1543 def format_milliseconds_in_day(self, num): 

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

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

1546 return self.format(msecs, num) 

1547 

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

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

1550 

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

1552 # reference date which is important to distinguish between timezone 

1553 # variants (summer/standard time) 

1554 value = self.value 

1555 if self.reference_date: 

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

1557 

1558 if char == 'z': 

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

1560 elif char == 'Z': 

1561 if num == 5: 

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

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

1564 elif char == 'O': 

1565 if num == 4: 

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

1567 # TODO: To add support for O:1 

1568 elif char == 'v': 

1569 return get_timezone_name(value.tzinfo, width, 

1570 locale=self.locale) 

1571 elif char == 'V': 

1572 if num == 1: 

1573 return get_timezone_name(value.tzinfo, width, 

1574 uncommon=True, locale=self.locale) 

1575 elif num == 2: 

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

1577 elif num == 3: 

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

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

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

1581 elif char == 'X': 

1582 if num == 1: 

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

1584 return_z=True) 

1585 elif num in (2, 4): 

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

1587 return_z=True) 

1588 elif num in (3, 5): 

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

1590 return_z=True) 

1591 elif char == 'x': 

1592 if num == 1: 

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

1594 elif num in (2, 4): 

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

1596 elif num in (3, 5): 

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

1598 

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

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

1601 

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

1603 if date is None: 

1604 date = self.value 

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

1606 

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

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

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

1610 

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

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

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

1614 

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

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

1617 1 

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

1619 2 

1620 

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

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

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

1624 current date is assumed 

1625 """ 

1626 if day_of_week is None: 

1627 day_of_week = self.value.weekday() 

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

1629 day_of_period + 1) % 7 

1630 if first_day < 0: 

1631 first_day += 7 

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

1633 

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

1635 week_number += 1 

1636 

1637 if self.locale.first_week_day == 0: 

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

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

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

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

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

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

1644 if week_number > max_weeks: 

1645 week_number -= max_weeks 

1646 

1647 return week_number 

1648 

1649 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1664} 

1665 

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

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

1668#: in order of decreasing magnitude. 

1669PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" 

1670 

1671 

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

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

1674 

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

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

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

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

1679 

1680 Pattern can contain literal strings in single quotes: 

1681 

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

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

1684 

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

1686 characters: 

1687 

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

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

1690 

1691 :param pattern: the formatting pattern to parse 

1692 """ 

1693 if isinstance(pattern, DateTimePattern): 

1694 return pattern 

1695 return _cached_parse_pattern(pattern) 

1696 

1697 

1698@lru_cache(maxsize=1024) 

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

1700 result = [] 

1701 

1702 for tok_type, tok_value in tokenize_pattern(pattern): 

1703 if tok_type == "chars": 

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

1705 elif tok_type == "field": 

1706 fieldchar, fieldnum = tok_value 

1707 limit = PATTERN_CHARS[fieldchar] 

1708 if limit and fieldnum not in limit: 

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

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

1711 else: 

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

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

1714 

1715 

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

1717 """ 

1718 Tokenize date format patterns. 

1719 

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

1721 

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

1723 

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

1725 

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

1727 

1728 :param pattern: Pattern string 

1729 :type pattern: str 

1730 :rtype: list[tuple] 

1731 """ 

1732 result = [] 

1733 quotebuf = None 

1734 charbuf = [] 

1735 fieldchar = [''] 

1736 fieldnum = [0] 

1737 

1738 def append_chars(): 

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

1740 del charbuf[:] 

1741 

1742 def append_field(): 

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

1744 fieldchar[0] = '' 

1745 fieldnum[0] = 0 

1746 

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

1748 if quotebuf is None: 

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

1750 if fieldchar[0]: 

1751 append_field() 

1752 elif charbuf: 

1753 append_chars() 

1754 quotebuf = [] 

1755 elif char in PATTERN_CHARS: 

1756 if charbuf: 

1757 append_chars() 

1758 if char == fieldchar[0]: 

1759 fieldnum[0] += 1 

1760 else: 

1761 if fieldchar[0]: 

1762 append_field() 

1763 fieldchar[0] = char 

1764 fieldnum[0] = 1 

1765 else: 

1766 if fieldchar[0]: 

1767 append_field() 

1768 charbuf.append(char) 

1769 

1770 elif quotebuf is not None: 

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

1772 charbuf.extend(quotebuf) 

1773 quotebuf = None 

1774 else: # inside quote 

1775 quotebuf.append(char) 

1776 

1777 if fieldchar[0]: 

1778 append_field() 

1779 elif charbuf: 

1780 append_chars() 

1781 

1782 return result 

1783 

1784 

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

1786 """ 

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

1788 

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

1790 

1791 :type tokens: Iterable[tuple] 

1792 :rtype: str 

1793 """ 

1794 output = [] 

1795 for tok_type, tok_value in tokens: 

1796 if tok_type == "field": 

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

1798 elif tok_type == "chars": 

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

1800 output.append(tok_value) 

1801 else: 

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

1803 return "".join(output) 

1804 

1805 

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

1807 """ 

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

1809 

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

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

1812 

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

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

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

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

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

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

1819 >>> split_interval_pattern("MMM d") 

1820 ['MMM d'] 

1821 >>> split_interval_pattern("y G") 

1822 ['y G'] 

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

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

1825 

1826 :param pattern: Interval pattern string 

1827 :return: list of "subpatterns" 

1828 """ 

1829 

1830 seen_fields = set() 

1831 parts = [[]] 

1832 

1833 for tok_type, tok_value in tokenize_pattern(pattern): 

1834 if tok_type == "field": 

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

1836 parts.append([]) 

1837 seen_fields.clear() 

1838 seen_fields.add(tok_value[0]) 

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

1840 

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

1842 

1843 

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

1845 """ 

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

1847 

1848 This uses the rules outlined in the TR35 document. 

1849 

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

1851 'yMd' 

1852 

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

1854 'jyMMd' 

1855 

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

1857 

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

1859 'hmv' 

1860 

1861 :param skeleton: The skeleton to match 

1862 :type skeleton: str 

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

1864 :type options: Iterable[str] 

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

1866 :rtype: str|None 

1867 """ 

1868 

1869 # TODO: maybe implement pattern expansion? 

1870 

1871 # Based on the implementation in 

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

1873 

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

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

1876 

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

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

1879 

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

1881 best_skeleton = None 

1882 best_distance = None 

1883 for option in options: 

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

1885 distance = 0 

1886 for field in PATTERN_CHARS: 

1887 input_width = get_input_field_width(field, 0) 

1888 opt_width = get_opt_field_width(field, 0) 

1889 if input_width == opt_width: 

1890 continue 

1891 if opt_width == 0 or input_width == 0: 

1892 if not allow_different_fields: # This one is not okay 

1893 option = None 

1894 break 

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

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

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

1898 else: 

1899 distance += abs(input_width - opt_width) 

1900 

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

1902 continue 

1903 

1904 if not best_skeleton or distance < best_distance: 

1905 best_skeleton = option 

1906 best_distance = distance 

1907 

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

1909 break 

1910 

1911 return best_skeleton