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

289 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-30 06:11 +0000

1from __future__ import annotations 

2 

3import datetime 

4import re 

5 

6from typing import TYPE_CHECKING 

7from typing import Any 

8from typing import Callable 

9from typing import ClassVar 

10from typing import Match 

11from typing import Sequence 

12from typing import cast 

13 

14import pendulum 

15 

16from pendulum.locales.locale import Locale 

17 

18 

19if TYPE_CHECKING: 

20 from pendulum import Timezone 

21 

22_MATCH_1 = r"\d" 

23_MATCH_2 = r"\d\d" 

24_MATCH_3 = r"\d{3}" 

25_MATCH_4 = r"\d{4}" 

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

27_MATCH_1_TO_2 = r"\d\d?" 

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

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

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

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

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

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

34_MATCH_UNSIGNED = r"\d+" 

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

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

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

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

39_MATCH_WORD = ( 

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

41 "['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+" 

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

43) 

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

45 

46 

47class Formatter: 

48 _TOKENS: str = ( 

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

50 "(" 

51 "Mo|MM?M?M?" 

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

53 "|E{1,4}" 

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

55 "|YYYY|YY|Y" 

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

57 "|a|A" 

58 "|hh?|HH?|kk?" 

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

60 "|x|X" 

61 "|zz?|ZZ?" 

62 "|LTS|LT|LL?L?L?" 

63 ")" 

64 ) 

65 

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

67 

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

69 

70 _LOCALIZABLE_TOKENS: ClassVar[ 

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

72 ] = { 

73 "Qo": None, 

74 "MMMM": "months.wide", 

75 "MMM": "months.abbreviated", 

76 "Mo": None, 

77 "DDDo": None, 

78 "Do": lambda locale: tuple( 

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

80 ), 

81 "dddd": "days.wide", 

82 "ddd": "days.abbreviated", 

83 "dd": "days.short", 

84 "do": None, 

85 "e": None, 

86 "eo": None, 

87 "Wo": None, 

88 "wo": None, 

89 "A": lambda locale: ( 

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

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

92 ), 

93 "a": lambda locale: ( 

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

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

96 ), 

97 } 

98 

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

100 # Year 

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

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

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

104 # Quarter 

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

106 # Month 

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

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

109 # Day 

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

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

112 # Day of Year 

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

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

115 # Day of Week 

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

117 # Day of ISO Week 

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

119 # Hour 

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

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

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

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

124 # Minute 

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

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

127 # Second 

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

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

130 # Fractional second 

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

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

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

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

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

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

137 # Timestamp 

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

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

140 # Timezone 

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

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

143 } 

144 

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

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

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

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

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

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

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

152 } 

153 

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

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

156 "LT": "h:mm A", 

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

158 "LL": "MMMM D, YYYY", 

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

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

161 } 

162 

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

164 "Y": _MATCH_SIGNED, 

165 "YY": (_MATCH_1_TO_2, _MATCH_2), 

166 "YYYY": (_MATCH_1_TO_4, _MATCH_4), 

167 "Q": _MATCH_1, 

168 "Qo": None, 

169 "M": _MATCH_1_TO_2, 

170 "MM": (_MATCH_1_TO_2, _MATCH_2), 

171 "MMM": _MATCH_WORD, 

172 "MMMM": _MATCH_WORD, 

173 "D": _MATCH_1_TO_2, 

174 "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2), 

175 "DDD": _MATCH_1_TO_3, 

176 "DDDD": _MATCH_3, 

177 "dddd": _MATCH_WORD, 

178 "ddd": _MATCH_WORD, 

179 "dd": _MATCH_WORD, 

180 "d": _MATCH_1, 

181 "e": _MATCH_1, 

182 "E": _MATCH_1, 

183 "Do": None, 

184 "H": _MATCH_1_TO_2, 

185 "HH": (_MATCH_1_TO_2, _MATCH_2), 

186 "h": _MATCH_1_TO_2, 

187 "hh": (_MATCH_1_TO_2, _MATCH_2), 

188 "m": _MATCH_1_TO_2, 

189 "mm": (_MATCH_1_TO_2, _MATCH_2), 

190 "s": _MATCH_1_TO_2, 

191 "ss": (_MATCH_1_TO_2, _MATCH_2), 

192 "S": (_MATCH_1_TO_3, _MATCH_1), 

193 "SS": (_MATCH_1_TO_3, _MATCH_2), 

194 "SSS": (_MATCH_1_TO_3, _MATCH_3), 

195 "SSSS": _MATCH_UNSIGNED, 

196 "SSSSS": _MATCH_UNSIGNED, 

197 "SSSSSS": _MATCH_UNSIGNED, 

198 "x": _MATCH_SIGNED, 

199 "X": _MATCH_TIMESTAMP, 

200 "ZZ": _MATCH_SHORT_OFFSET, 

201 "Z": _MATCH_OFFSET, 

202 "z": _MATCH_TIMEZONE, 

203 } 

204 

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

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

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

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

209 "MMMM": lambda month: month, 

210 "MMM": lambda month: month, 

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

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

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

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

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

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

217 "dddd": lambda weekday: weekday, 

218 "ddd": lambda weekday: weekday, 

219 "dd": lambda weekday: weekday, 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

236 "a": lambda meridiem: meridiem, 

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

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

239 "ZZ": str, 

240 "Z": str, 

241 "z": str, 

242 } 

243 

244 def format( 

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

246 ) -> str: 

247 """ 

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

249 

250 :param dt: The instance to format 

251 :param fmt: The format to use 

252 :param locale: The locale to use 

253 """ 

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

255 

256 result = self._FORMAT_RE.sub( 

257 lambda m: m.group(1) 

258 if m.group(1) 

259 else m.group(2) 

260 if m.group(2) 

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

262 fmt, 

263 ) 

264 

265 return result 

266 

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

268 """ 

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

270 

271 :param dt: The instance to format 

272 :param token: The token to use 

273 :param locale: The locale to use 

274 """ 

275 if token in self._DATE_FORMATS: 

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

277 if fmt is None: 

278 fmt = self._DEFAULT_DATE_FORMATS[token] 

279 

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

281 

282 if token in self._LOCALIZABLE_TOKENS: 

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

284 

285 if token in self._TOKENS_RULES: 

286 return self._TOKENS_RULES[token](dt) 

287 

288 # Timezone 

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

290 if dt.tzinfo is None: 

291 return "" 

292 

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

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

295 minutes = offset.total_seconds() / 60 

296 

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

298 

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

300 

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

302 

303 return token 

304 

305 def _format_localizable_token( 

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

307 ) -> str: 

308 """ 

309 Formats a DateTime instance 

310 with a given localizable token and locale. 

311 

312 :param dt: The instance to format 

313 :param token: The token to use 

314 :param locale: The locale to use 

315 """ 

316 if token == "MMM": 

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

318 elif token == "MMMM": 

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

320 elif token == "dd": 

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

322 elif token == "ddd": 

323 return cast( 

324 str, 

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

326 ) 

327 elif token == "dddd": 

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

329 elif token == "e": 

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

331 

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

333 elif token == "Do": 

334 return locale.ordinalize(dt.day) 

335 elif token == "do": 

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

337 elif token == "Mo": 

338 return locale.ordinalize(dt.month) 

339 elif token == "Qo": 

340 return locale.ordinalize(dt.quarter) 

341 elif token == "wo": 

342 return locale.ordinalize(dt.week_of_year) 

343 elif token == "DDDo": 

344 return locale.ordinalize(dt.day_of_year) 

345 elif token == "eo": 

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

347 

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

349 elif token == "A": 

350 key = "translations.day_periods" 

351 if dt.hour >= 12: 

352 key += ".pm" 

353 else: 

354 key += ".am" 

355 

356 return cast(str, locale.get(key)) 

357 else: 

358 return token 

359 

360 def parse( 

361 self, 

362 time: str, 

363 fmt: str, 

364 now: pendulum.DateTime, 

365 locale: str | None = None, 

366 ) -> dict[str, Any]: 

367 """ 

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

369 

370 :param time: The timestring 

371 :param fmt: The format 

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

373 :param locale: The locale to use 

374 

375 :return: The parsed elements 

376 """ 

377 escaped_fmt = re.escape(fmt) 

378 

379 tokens = self._FROM_FORMAT_RE.findall(escaped_fmt) 

380 if not tokens: 

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.search("^" + 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