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

289 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 tokens = self._FROM_FORMAT_RE.findall(escaped_fmt) 

381 if not tokens: 

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

383 

384 if not locale: 

385 locale = pendulum.get_locale() 

386 

387 loaded_locale: Locale = Locale.load(locale) 

388 

389 parsed = { 

390 "year": None, 

391 "month": None, 

392 "day": None, 

393 "hour": None, 

394 "minute": None, 

395 "second": None, 

396 "microsecond": None, 

397 "tz": None, 

398 "quarter": None, 

399 "day_of_week": None, 

400 "day_of_year": None, 

401 "meridiem": None, 

402 "timestamp": None, 

403 } 

404 

405 pattern = self._FROM_FORMAT_RE.sub( 

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

407 ) 

408 

409 if not re.search("^" + pattern + "$", time): 

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

411 

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

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

414 

415 re.sub(pattern, _get_parsed_values, time) 

416 

417 return self._check_parsed(parsed, now) 

418 

419 def _check_parsed( 

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

421 ) -> dict[str, Any]: 

422 """ 

423 Checks validity of parsed elements. 

424 

425 :param parsed: The elements to parse. 

426 

427 :return: The validated elements. 

428 """ 

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

430 "year": parsed["year"], 

431 "month": parsed["month"], 

432 "day": parsed["day"], 

433 "hour": parsed["hour"], 

434 "minute": parsed["minute"], 

435 "second": parsed["second"], 

436 "microsecond": parsed["microsecond"], 

437 "tz": None, 

438 } 

439 

440 # If timestamp has been specified 

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

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

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

444 if "." in str_us: 

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

446 else: 

447 microseconds = 0 

448 

449 from pendulum.helpers import local_time 

450 

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

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

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

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

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

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

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

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

459 

460 return validated 

461 

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

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

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

465 else: 

466 dt = now 

467 

468 dt = dt.start_of("year") 

469 

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

471 dt = dt.add(months=3) 

472 

473 validated["year"] = dt.year 

474 validated["month"] = dt.month 

475 validated["day"] = dt.day 

476 

477 if validated["year"] is None: 

478 validated["year"] = now.year 

479 

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

481 dt = cast( 

482 "pendulum.DateTime", 

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

484 ) 

485 

486 validated["month"] = dt.month 

487 validated["day"] = dt.day 

488 

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

490 dt = pendulum.datetime( 

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

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

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

494 ) 

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

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

497 validated["year"] = dt.year 

498 validated["month"] = dt.month 

499 validated["day"] = dt.day 

500 

501 # Meridiem 

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

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

504 # This is not valid 

505 if validated["hour"] is None: 

506 raise ValueError("Invalid Date") 

507 

508 t = ( 

509 validated["hour"], 

510 validated["minute"], 

511 validated["second"], 

512 validated["microsecond"], 

513 ) 

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

515 raise ValueError("Invalid date") 

516 

517 pm = parsed["meridiem"] == "pm" 

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

519 if pm: 

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

521 

522 if validated["month"] is None: 

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

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

525 else: 

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

527 

528 if validated["day"] is None: 

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

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

531 else: 

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

533 

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

535 if validated[part] is None: 

536 validated[part] = 0 

537 

538 validated["tz"] = parsed["tz"] 

539 

540 return validated 

541 

542 def _get_parsed_values( 

543 self, 

544 m: Match[str], 

545 parsed: dict[str, Any], 

546 locale: Locale, 

547 now: pendulum.DateTime, 

548 ) -> None: 

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

550 if token in self._LOCALIZABLE_TOKENS: 

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

552 else: 

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

554 

555 def _get_parsed_value( 

556 self, 

557 token: str, 

558 value: str, 

559 parsed: dict[str, Any], 

560 now: pendulum.DateTime, 

561 ) -> None: 

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

563 

564 if "Y" in token: 

565 if token == "YY": 

566 if parsed_token <= 68: 

567 parsed_token += 2000 

568 else: 

569 parsed_token += 1900 

570 

571 parsed["year"] = parsed_token 

572 elif token == "Q": 

573 parsed["quarter"] = parsed_token 

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

575 parsed["month"] = parsed_token 

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

577 parsed["day_of_year"] = parsed_token 

578 elif "D" in token: 

579 parsed["day"] = parsed_token 

580 elif "H" in token: 

581 parsed["hour"] = parsed_token 

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

583 if parsed_token > 12: 

584 raise ValueError("Invalid date") 

585 

586 parsed["hour"] = parsed_token 

587 elif "m" in token: 

588 parsed["minute"] = parsed_token 

589 elif "s" in token: 

590 parsed["second"] = parsed_token 

591 elif "S" in token: 

592 parsed["microsecond"] = parsed_token 

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

594 parsed["day_of_week"] = parsed_token 

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

596 parsed["timestamp"] = parsed_token 

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

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

599 tz = value[1:] 

600 if ":" not in tz: 

601 if len(tz) == 2: 

602 tz = f"{tz}00" 

603 

604 off_hour = tz[0:2] 

605 off_minute = tz[2:4] 

606 else: 

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

608 

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

610 

611 if negative: 

612 offset = -1 * offset 

613 

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

615 elif token == "z": 

616 # Full timezone 

617 if value not in pendulum.timezones(): 

618 raise ValueError("Invalid date") 

619 

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

621 

622 def _get_parsed_locale_value( 

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

624 ) -> None: 

625 if token == "MMMM": 

626 unit = "month" 

627 match = "months.wide" 

628 elif token == "MMM": 

629 unit = "month" 

630 match = "months.abbreviated" 

631 elif token == "Do": 

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

633 

634 return 

635 elif token == "dddd": 

636 unit = "day_of_week" 

637 match = "days.wide" 

638 elif token == "ddd": 

639 unit = "day_of_week" 

640 match = "days.abbreviated" 

641 elif token == "dd": 

642 unit = "day_of_week" 

643 match = "days.short" 

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

645 valid_values = [ 

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

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

648 ] 

649 

650 if token == "a": 

651 value = value.lower() 

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

653 

654 if value not in valid_values: 

655 raise ValueError("Invalid date") 

656 

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

658 

659 return 

660 else: 

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

662 

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

664 if value is None: 

665 raise ValueError("Invalid date") 

666 

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

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

669 return token[1:-1] 

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

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

672 return "" 

673 

674 return token 

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

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

677 

678 if token in self._LOCALIZABLE_TOKENS: 

679 values = self._LOCALIZABLE_TOKENS[token] 

680 if callable(values): 

681 candidates = values(locale) 

682 else: 

683 candidates = tuple( 

684 locale.translation( 

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

686 ).values() 

687 ) 

688 else: 

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

690 

691 if not candidates: 

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

693 

694 if not isinstance(candidates, tuple): 

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

696 

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

698 

699 return pattern