Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pendulum/formatting/formatter.py: 17%

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

288 statements  

1from __future__ import annotations 

2 

3import datetime 

4import re 

5 

6from re import Match 

7from typing import TYPE_CHECKING 

8from typing import Any 

9from typing import Callable 

10from typing import ClassVar 

11from typing import cast 

12 

13import pendulum 

14 

15from pendulum.locales.locale import Locale 

16 

17 

18if TYPE_CHECKING: 

19 from collections.abc import Sequence 

20 

21 from pendulum import Timezone 

22 

23_MATCH_1 = r"\d" 

24_MATCH_2 = r"\d\d" 

25_MATCH_3 = r"\d{3}" 

26_MATCH_4 = r"\d{4}" 

27_MATCH_6 = r"[+-]?\d{6}" 

28_MATCH_1_TO_2 = r"\d\d?" 

29_MATCH_1_TO_2_LEFT_PAD = r"[0-9 ]\d?" 

30_MATCH_1_TO_3 = r"\d{1,3}" 

31_MATCH_1_TO_4 = r"\d{1,4}" 

32_MATCH_1_TO_6 = r"[+-]?\d{1,6}" 

33_MATCH_3_TO_4 = r"\d{3}\d?" 

34_MATCH_5_TO_6 = r"\d{5}\d?" 

35_MATCH_UNSIGNED = r"\d+" 

36_MATCH_SIGNED = r"[+-]?\d+" 

37_MATCH_OFFSET = r"[Zz]|[+-]\d\d:?\d\d" 

38_MATCH_SHORT_OFFSET = r"[Zz]|[+-]\d\d(?::?\d\d)?" 

39_MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?" 

40_MATCH_WORD = ( 

41 "(?i)[0-9]*" 

42 "['a-z\u00a0-\u05ff\u0700-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]+" 

43 r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}" 

44) 

45_MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?" 

46 

47 

48class Formatter: 

49 _TOKENS: str = ( 

50 r"\[([^\[]*)\]|\\(.)|" 

51 "(" 

52 "Mo|MM?M?M?" 

53 "|Do|DDDo|DD?D?D?|ddd?d?|do?|eo?" 

54 "|E{1,4}" 

55 "|w[o|w]?|W[o|W]?|Qo?" 

56 "|YYYY|YY|Y" 

57 "|gg(ggg?)?|GG(GGG?)?" 

58 "|a|A" 

59 "|hh?|HH?|kk?" 

60 "|mm?|ss?|S{1,9}" 

61 "|x|X" 

62 "|zz?|ZZ?" 

63 "|LTS|LT|LL?L?L?" 

64 ")" 

65 ) 

66 

67 _FORMAT_RE: re.Pattern[str] = re.compile(_TOKENS) 

68 

69 _FROM_FORMAT_RE: re.Pattern[str] = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])") 

70 

71 _LOCALIZABLE_TOKENS: ClassVar[ 

72 dict[str, str | Callable[[Locale], Sequence[str]] | None] 

73 ] = { 

74 "Qo": None, 

75 "MMMM": "months.wide", 

76 "MMM": "months.abbreviated", 

77 "Mo": None, 

78 "DDDo": None, 

79 "Do": lambda locale: tuple( 

80 rf"\d+{o}" for o in locale.get("custom.ordinal").values() 

81 ), 

82 "dddd": "days.wide", 

83 "ddd": "days.abbreviated", 

84 "dd": "days.short", 

85 "do": None, 

86 "e": None, 

87 "eo": None, 

88 "Wo": None, 

89 "wo": None, 

90 "A": lambda locale: ( 

91 locale.translation("day_periods.am"), 

92 locale.translation("day_periods.pm"), 

93 ), 

94 "a": lambda locale: ( 

95 locale.translation("day_periods.am").lower(), 

96 locale.translation("day_periods.pm").lower(), 

97 ), 

98 } 

99 

100 _TOKENS_RULES: ClassVar[dict[str, Callable[[pendulum.DateTime], str]]] = { 

101 # Year 

102 "YYYY": lambda dt: f"{dt.year:d}", 

103 "YY": lambda dt: f"{dt.year:d}"[2:], 

104 "Y": lambda dt: f"{dt.year:d}", 

105 # Quarter 

106 "Q": lambda dt: f"{dt.quarter:d}", 

107 # Month 

108 "MM": lambda dt: f"{dt.month:02d}", 

109 "M": lambda dt: f"{dt.month:d}", 

110 # Day 

111 "DD": lambda dt: f"{dt.day:02d}", 

112 "D": lambda dt: f"{dt.day:d}", 

113 # Day of Year 

114 "DDDD": lambda dt: f"{dt.day_of_year:03d}", 

115 "DDD": lambda dt: f"{dt.day_of_year:d}", 

116 # Day of Week 

117 "d": lambda dt: f"{(dt.day_of_week + 1) % 7:d}", 

118 # Day of ISO Week 

119 "E": lambda dt: f"{dt.isoweekday():d}", 

120 # Hour 

121 "HH": lambda dt: f"{dt.hour:02d}", 

122 "H": lambda dt: f"{dt.hour:d}", 

123 "hh": lambda dt: f"{dt.hour % 12 or 12:02d}", 

124 "h": lambda dt: f"{dt.hour % 12 or 12:d}", 

125 # Minute 

126 "mm": lambda dt: f"{dt.minute:02d}", 

127 "m": lambda dt: f"{dt.minute:d}", 

128 # Second 

129 "ss": lambda dt: f"{dt.second:02d}", 

130 "s": lambda dt: f"{dt.second:d}", 

131 # Fractional second 

132 "S": lambda dt: f"{dt.microsecond // 100000:01d}", 

133 "SS": lambda dt: f"{dt.microsecond // 10000:02d}", 

134 "SSS": lambda dt: f"{dt.microsecond // 1000:03d}", 

135 "SSSS": lambda dt: f"{dt.microsecond // 100:04d}", 

136 "SSSSS": lambda dt: f"{dt.microsecond // 10:05d}", 

137 "SSSSSS": lambda dt: f"{dt.microsecond:06d}", 

138 # Timestamp 

139 "X": lambda dt: f"{dt.int_timestamp:d}", 

140 "x": lambda dt: f"{dt.int_timestamp * 1000 + dt.microsecond // 1000:d}", 

141 # Timezone 

142 "zz": lambda dt: f"{dt.tzname() if dt.tzinfo is not None else ''}", 

143 "z": lambda dt: f"{dt.timezone_name or ''}", 

144 } 

145 

146 _DATE_FORMATS: ClassVar[dict[str, str]] = { 

147 "LTS": "formats.time.full", 

148 "LT": "formats.time.short", 

149 "L": "formats.date.short", 

150 "LL": "formats.date.long", 

151 "LLL": "formats.datetime.long", 

152 "LLLL": "formats.datetime.full", 

153 } 

154 

155 _DEFAULT_DATE_FORMATS: ClassVar[dict[str, str]] = { 

156 "LTS": "h:mm:ss A", 

157 "LT": "h:mm A", 

158 "L": "MM/DD/YYYY", 

159 "LL": "MMMM D, YYYY", 

160 "LLL": "MMMM D, YYYY h:mm A", 

161 "LLLL": "dddd, MMMM D, YYYY h:mm A", 

162 } 

163 

164 _REGEX_TOKENS: ClassVar[dict[str, str | Sequence[str] | None]] = { 

165 "Y": _MATCH_SIGNED, 

166 "YY": (_MATCH_1_TO_2, _MATCH_2), 

167 "YYYY": (_MATCH_1_TO_4, _MATCH_4), 

168 "Q": _MATCH_1, 

169 "Qo": None, 

170 "M": _MATCH_1_TO_2, 

171 "MM": (_MATCH_1_TO_2, _MATCH_2), 

172 "MMM": _MATCH_WORD, 

173 "MMMM": _MATCH_WORD, 

174 "D": _MATCH_1_TO_2, 

175 "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2), 

176 "DDD": _MATCH_1_TO_3, 

177 "DDDD": _MATCH_3, 

178 "dddd": _MATCH_WORD, 

179 "ddd": _MATCH_WORD, 

180 "dd": _MATCH_WORD, 

181 "d": _MATCH_1, 

182 "e": _MATCH_1, 

183 "E": _MATCH_1, 

184 "Do": None, 

185 "H": _MATCH_1_TO_2, 

186 "HH": (_MATCH_1_TO_2, _MATCH_2), 

187 "h": _MATCH_1_TO_2, 

188 "hh": (_MATCH_1_TO_2, _MATCH_2), 

189 "m": _MATCH_1_TO_2, 

190 "mm": (_MATCH_1_TO_2, _MATCH_2), 

191 "s": _MATCH_1_TO_2, 

192 "ss": (_MATCH_1_TO_2, _MATCH_2), 

193 "S": (_MATCH_1_TO_3, _MATCH_1), 

194 "SS": (_MATCH_1_TO_3, _MATCH_2), 

195 "SSS": (_MATCH_1_TO_3, _MATCH_3), 

196 "SSSS": _MATCH_UNSIGNED, 

197 "SSSSS": _MATCH_UNSIGNED, 

198 "SSSSSS": _MATCH_UNSIGNED, 

199 "x": _MATCH_SIGNED, 

200 "X": _MATCH_TIMESTAMP, 

201 "ZZ": _MATCH_SHORT_OFFSET, 

202 "Z": _MATCH_OFFSET, 

203 "z": _MATCH_TIMEZONE, 

204 } 

205 

206 _PARSE_TOKENS: ClassVar[dict[str, Callable[[str], Any]]] = { 

207 "YYYY": lambda year: int(year), 

208 "YY": lambda year: int(year), 

209 "Q": lambda quarter: int(quarter), 

210 "MMMM": lambda month: month, 

211 "MMM": lambda month: month, 

212 "MM": lambda month: int(month), 

213 "M": lambda month: int(month), 

214 "DDDD": lambda day: int(day), 

215 "DDD": lambda day: int(day), 

216 "DD": lambda day: int(day), 

217 "D": lambda day: int(day), 

218 "dddd": lambda weekday: weekday, 

219 "ddd": lambda weekday: weekday, 

220 "dd": lambda weekday: weekday, 

221 "d": lambda weekday: int(weekday), 

222 "E": lambda weekday: int(weekday) - 1, 

223 "HH": lambda hour: int(hour), 

224 "H": lambda hour: int(hour), 

225 "hh": lambda hour: int(hour), 

226 "h": lambda hour: int(hour), 

227 "mm": lambda minute: int(minute), 

228 "m": lambda minute: int(minute), 

229 "ss": lambda second: int(second), 

230 "s": lambda second: int(second), 

231 "S": lambda us: int(us) * 100000, 

232 "SS": lambda us: int(us) * 10000, 

233 "SSS": lambda us: int(us) * 1000, 

234 "SSSS": lambda us: int(us) * 100, 

235 "SSSSS": lambda us: int(us) * 10, 

236 "SSSSSS": lambda us: int(us), 

237 "a": lambda meridiem: meridiem, 

238 "X": lambda ts: float(ts), 

239 "x": lambda ts: float(ts) / 1e3, 

240 "ZZ": str, 

241 "Z": str, 

242 "z": str, 

243 } 

244 

245 def format( 

246 self, dt: pendulum.DateTime, fmt: str, locale: str | Locale | None = None 

247 ) -> str: 

248 """ 

249 Formats a DateTime instance with a given format and locale. 

250 

251 :param dt: The instance to format 

252 :param fmt: The format to use 

253 :param locale: The locale to use 

254 """ 

255 loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) 

256 

257 result = self._FORMAT_RE.sub( 

258 lambda m: m.group(1) 

259 if m.group(1) 

260 else m.group(2) 

261 if m.group(2) 

262 else self._format_token(dt, m.group(3), loaded_locale), 

263 fmt, 

264 ) 

265 

266 return result 

267 

268 def _format_token(self, dt: pendulum.DateTime, token: str, locale: Locale) -> str: 

269 """ 

270 Formats a DateTime instance with a given token and locale. 

271 

272 :param dt: The instance to format 

273 :param token: The token to use 

274 :param locale: The locale to use 

275 """ 

276 if token in self._DATE_FORMATS: 

277 fmt = locale.get(f"custom.date_formats.{token}") 

278 if fmt is None: 

279 fmt = self._DEFAULT_DATE_FORMATS[token] 

280 

281 return self.format(dt, fmt, locale) 

282 

283 if token in self._LOCALIZABLE_TOKENS: 

284 return self._format_localizable_token(dt, token, locale) 

285 

286 if token in self._TOKENS_RULES: 

287 return self._TOKENS_RULES[token](dt) 

288 

289 # Timezone 

290 if token in ["ZZ", "Z"]: 

291 if dt.tzinfo is None: 

292 return "" 

293 

294 separator = ":" if token == "Z" else "" 

295 offset = dt.utcoffset() or datetime.timedelta() 

296 minutes = offset.total_seconds() / 60 

297 

298 sign = "+" if minutes >= 0 else "-" 

299 

300 hour, minute = divmod(abs(int(minutes)), 60) 

301 

302 return f"{sign}{hour:02d}{separator}{minute:02d}" 

303 

304 return token 

305 

306 def _format_localizable_token( 

307 self, dt: pendulum.DateTime, token: str, locale: Locale 

308 ) -> str: 

309 """ 

310 Formats a DateTime instance 

311 with a given localizable token and locale. 

312 

313 :param dt: The instance to format 

314 :param token: The token to use 

315 :param locale: The locale to use 

316 """ 

317 if token == "MMM": 

318 return cast("str", locale.get("translations.months.abbreviated")[dt.month]) 

319 elif token == "MMMM": 

320 return cast("str", locale.get("translations.months.wide")[dt.month]) 

321 elif token == "dd": 

322 return cast("str", locale.get("translations.days.short")[dt.day_of_week]) 

323 elif token == "ddd": 

324 return cast( 

325 "str", 

326 locale.get("translations.days.abbreviated")[dt.day_of_week], 

327 ) 

328 elif token == "dddd": 

329 return cast("str", locale.get("translations.days.wide")[dt.day_of_week]) 

330 elif token == "e": 

331 first_day = cast("int", locale.get("translations.week_data.first_day")) 

332 

333 return str((dt.day_of_week % 7 - first_day) % 7) 

334 elif token == "Do": 

335 return locale.ordinalize(dt.day) 

336 elif token == "do": 

337 return locale.ordinalize((dt.day_of_week + 1) % 7) 

338 elif token == "Mo": 

339 return locale.ordinalize(dt.month) 

340 elif token == "Qo": 

341 return locale.ordinalize(dt.quarter) 

342 elif token == "wo": 

343 return locale.ordinalize(dt.week_of_year) 

344 elif token == "DDDo": 

345 return locale.ordinalize(dt.day_of_year) 

346 elif token == "eo": 

347 first_day = cast("int", locale.get("translations.week_data.first_day")) 

348 

349 return locale.ordinalize((dt.day_of_week % 7 - first_day) % 7 + 1) 

350 elif token == "A": 

351 key = "translations.day_periods" 

352 if dt.hour >= 12: 

353 key += ".pm" 

354 else: 

355 key += ".am" 

356 

357 return cast("str", locale.get(key)) 

358 else: 

359 return token 

360 

361 def parse( 

362 self, 

363 time: str, 

364 fmt: str, 

365 now: pendulum.DateTime, 

366 locale: str | None = None, 

367 ) -> dict[str, Any]: 

368 """ 

369 Parses a time string matching a given format as a tuple. 

370 

371 :param time: The timestring 

372 :param fmt: The format 

373 :param now: The datetime to use as "now" 

374 :param locale: The locale to use 

375 

376 :return: The parsed elements 

377 """ 

378 escaped_fmt = re.escape(fmt) 

379 

380 if not self._FROM_FORMAT_RE.search(escaped_fmt): 

381 raise ValueError("The given time string does not match the given format") 

382 

383 if not locale: 

384 locale = pendulum.get_locale() 

385 

386 loaded_locale: Locale = Locale.load(locale) 

387 

388 parsed = { 

389 "year": None, 

390 "month": None, 

391 "day": None, 

392 "hour": None, 

393 "minute": None, 

394 "second": None, 

395 "microsecond": None, 

396 "tz": None, 

397 "quarter": None, 

398 "day_of_week": None, 

399 "day_of_year": None, 

400 "meridiem": None, 

401 "timestamp": None, 

402 } 

403 

404 pattern = self._FROM_FORMAT_RE.sub( 

405 lambda m: self._replace_tokens(m.group(0), loaded_locale), escaped_fmt 

406 ) 

407 

408 if not re.fullmatch(pattern, time): 

409 raise ValueError(f"String does not match format {fmt}") 

410 

411 def _get_parsed_values(m: Match[str]) -> Any: 

412 return self._get_parsed_values(m, parsed, loaded_locale, now) 

413 

414 re.sub(pattern, _get_parsed_values, time) 

415 

416 return self._check_parsed(parsed, now) 

417 

418 def _check_parsed( 

419 self, parsed: dict[str, Any], now: pendulum.DateTime 

420 ) -> dict[str, Any]: 

421 """ 

422 Checks validity of parsed elements. 

423 

424 :param parsed: The elements to parse. 

425 

426 :return: The validated elements. 

427 """ 

428 validated: dict[str, int | Timezone | None] = { 

429 "year": parsed["year"], 

430 "month": parsed["month"], 

431 "day": parsed["day"], 

432 "hour": parsed["hour"], 

433 "minute": parsed["minute"], 

434 "second": parsed["second"], 

435 "microsecond": parsed["microsecond"], 

436 "tz": None, 

437 } 

438 

439 # If timestamp has been specified 

440 # we use it and don't go any further 

441 if parsed["timestamp"] is not None: 

442 str_us = str(parsed["timestamp"]) 

443 if "." in str_us: 

444 microseconds = int(f"{str_us.split('.')[1].ljust(6, '0')}") 

445 else: 

446 microseconds = 0 

447 

448 from pendulum.helpers import local_time 

449 

450 time = local_time(parsed["timestamp"], 0, microseconds) 

451 validated["year"] = time[0] 

452 validated["month"] = time[1] 

453 validated["day"] = time[2] 

454 validated["hour"] = time[3] 

455 validated["minute"] = time[4] 

456 validated["second"] = time[5] 

457 validated["microsecond"] = time[6] 

458 

459 return validated 

460 

461 if parsed["quarter"] is not None: 

462 if validated["year"] is not None: 

463 dt = pendulum.datetime(cast("int", validated["year"]), 1, 1) 

464 else: 

465 dt = now 

466 

467 dt = dt.start_of("year") 

468 

469 while dt.quarter != parsed["quarter"]: 

470 dt = dt.add(months=3) 

471 

472 validated["year"] = dt.year 

473 validated["month"] = dt.month 

474 validated["day"] = dt.day 

475 

476 if validated["year"] is None: 

477 validated["year"] = now.year 

478 

479 if parsed["day_of_year"] is not None: 

480 dt = cast( 

481 "pendulum.DateTime", 

482 pendulum.parse(f"{validated['year']}-{parsed['day_of_year']:>03d}"), 

483 ) 

484 

485 validated["month"] = dt.month 

486 validated["day"] = dt.day 

487 

488 if parsed["day_of_week"] is not None: 

489 dt = pendulum.datetime( 

490 cast("int", validated["year"]), 

491 cast("int", validated["month"]) or now.month, 

492 cast("int", validated["day"]) or now.day, 

493 ) 

494 dt = dt.start_of("week").subtract(days=1) 

495 dt = dt.next(parsed["day_of_week"]) 

496 validated["year"] = dt.year 

497 validated["month"] = dt.month 

498 validated["day"] = dt.day 

499 

500 # Meridiem 

501 if parsed["meridiem"] is not None: 

502 # If the time is greater than 13:00:00 

503 # This is not valid 

504 if validated["hour"] is None: 

505 raise ValueError("Invalid Date") 

506 

507 t = ( 

508 validated["hour"], 

509 validated["minute"], 

510 validated["second"], 

511 validated["microsecond"], 

512 ) 

513 if t >= (13, 0, 0, 0): 

514 raise ValueError("Invalid date") 

515 

516 pm = parsed["meridiem"] == "pm" 

517 validated["hour"] %= 12 # type: ignore[operator] 

518 if pm: 

519 validated["hour"] += 12 # type: ignore[operator] 

520 

521 if validated["month"] is None: 

522 if parsed["year"] is not None: 

523 validated["month"] = parsed["month"] or 1 

524 else: 

525 validated["month"] = parsed["month"] or now.month 

526 

527 if validated["day"] is None: 

528 if parsed["year"] is not None or parsed["month"] is not None: 

529 validated["day"] = parsed["day"] or 1 

530 else: 

531 validated["day"] = parsed["day"] or now.day 

532 

533 for part in ["hour", "minute", "second", "microsecond"]: 

534 if validated[part] is None: 

535 validated[part] = 0 

536 

537 validated["tz"] = parsed["tz"] 

538 

539 return validated 

540 

541 def _get_parsed_values( 

542 self, 

543 m: Match[str], 

544 parsed: dict[str, Any], 

545 locale: Locale, 

546 now: pendulum.DateTime, 

547 ) -> None: 

548 for token, index in m.re.groupindex.items(): 

549 if token in self._LOCALIZABLE_TOKENS: 

550 self._get_parsed_locale_value(token, m.group(index), parsed, locale) 

551 else: 

552 self._get_parsed_value(token, m.group(index), parsed, now) 

553 

554 def _get_parsed_value( 

555 self, 

556 token: str, 

557 value: str, 

558 parsed: dict[str, Any], 

559 now: pendulum.DateTime, 

560 ) -> None: 

561 parsed_token = self._PARSE_TOKENS[token](value) 

562 

563 if "Y" in token: 

564 if token == "YY": 

565 if parsed_token <= 68: 

566 parsed_token += 2000 

567 else: 

568 parsed_token += 1900 

569 

570 parsed["year"] = parsed_token 

571 elif token == "Q": 

572 parsed["quarter"] = parsed_token 

573 elif token in ["MM", "M"]: 

574 parsed["month"] = parsed_token 

575 elif token in ["DDDD", "DDD"]: 

576 parsed["day_of_year"] = parsed_token 

577 elif "D" in token: 

578 parsed["day"] = parsed_token 

579 elif "H" in token: 

580 parsed["hour"] = parsed_token 

581 elif token in ["hh", "h"]: 

582 if parsed_token > 12: 

583 raise ValueError("Invalid date") 

584 

585 parsed["hour"] = parsed_token 

586 elif "m" in token: 

587 parsed["minute"] = parsed_token 

588 elif "s" in token: 

589 parsed["second"] = parsed_token 

590 elif "S" in token: 

591 parsed["microsecond"] = parsed_token 

592 elif token in ["d", "E"]: 

593 parsed["day_of_week"] = parsed_token 

594 elif token in ["X", "x"]: 

595 parsed["timestamp"] = parsed_token 

596 elif token in ["ZZ", "Z"]: 

597 negative = bool(value.startswith("-")) 

598 tz = value[1:] 

599 if ":" not in tz: 

600 if len(tz) == 2: 

601 tz = f"{tz}00" 

602 

603 off_hour = tz[0:2] 

604 off_minute = tz[2:4] 

605 else: 

606 off_hour, off_minute = tz.split(":") 

607 

608 offset = ((int(off_hour) * 60) + int(off_minute)) * 60 

609 

610 if negative: 

611 offset = -1 * offset 

612 

613 parsed["tz"] = pendulum.timezone(offset) 

614 elif token == "z": 

615 # Full timezone 

616 if value not in pendulum.timezones(): 

617 raise ValueError("Invalid date") 

618 

619 parsed["tz"] = pendulum.timezone(value) 

620 

621 def _get_parsed_locale_value( 

622 self, token: str, value: str, parsed: dict[str, Any], locale: Locale 

623 ) -> None: 

624 if token == "MMMM": 

625 unit = "month" 

626 match = "months.wide" 

627 elif token == "MMM": 

628 unit = "month" 

629 match = "months.abbreviated" 

630 elif token == "Do": 

631 parsed["day"] = int(cast("Match[str]", re.match(r"(\d+)", value)).group(1)) 

632 

633 return 

634 elif token == "dddd": 

635 unit = "day_of_week" 

636 match = "days.wide" 

637 elif token == "ddd": 

638 unit = "day_of_week" 

639 match = "days.abbreviated" 

640 elif token == "dd": 

641 unit = "day_of_week" 

642 match = "days.short" 

643 elif token in ["a", "A"]: 

644 valid_values = [ 

645 locale.translation("day_periods.am"), 

646 locale.translation("day_periods.pm"), 

647 ] 

648 

649 if token == "a": 

650 value = value.lower() 

651 valid_values = [x.lower() for x in valid_values] 

652 

653 if value not in valid_values: 

654 raise ValueError("Invalid date") 

655 

656 parsed["meridiem"] = ["am", "pm"][valid_values.index(value)] 

657 

658 return 

659 else: 

660 raise ValueError(f'Invalid token "{token}"') 

661 

662 parsed[unit] = locale.match_translation(match, value) 

663 if value is None: 

664 raise ValueError("Invalid date") 

665 

666 def _replace_tokens(self, token: str, locale: Locale) -> str: 

667 if token.startswith("[") and token.endswith("]"): 

668 return token[1:-1] 

669 elif token.startswith("\\"): 

670 if len(token) == 2 and token[1] in {"[", "]"}: 

671 return "" 

672 

673 return token 

674 elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS: 

675 raise ValueError(f"Unsupported token: {token}") 

676 

677 if token in self._LOCALIZABLE_TOKENS: 

678 values = self._LOCALIZABLE_TOKENS[token] 

679 if callable(values): 

680 candidates = values(locale) 

681 else: 

682 candidates = tuple( 

683 locale.translation( 

684 cast("str", self._LOCALIZABLE_TOKENS[token]) 

685 ).values() 

686 ) 

687 else: 

688 candidates = cast("Sequence[str]", self._REGEX_TOKENS[token]) 

689 

690 if not candidates: 

691 raise ValueError(f"Unsupported token: {token}") 

692 

693 if not isinstance(candidates, tuple): 

694 candidates = (cast("str", candidates),) 

695 

696 pattern = f"(?P<{token}>{'|'.join(candidates)})" 

697 

698 return pattern