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

275 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +0000

1# -*- coding: utf-8 -*- 

2from __future__ import unicode_literals 

3 

4import datetime 

5import re 

6import typing 

7 

8import pendulum 

9 

10from pendulum.locales.locale import Locale 

11from pendulum.utils._compat import decode 

12 

13 

14_MATCH_1 = r"\d" 

15_MATCH_2 = r"\d\d" 

16_MATCH_3 = r"\d{3}" 

17_MATCH_4 = r"\d{4}" 

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

19_MATCH_1_TO_2 = r"\d\d?" 

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

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

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

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

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

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

26_MATCH_UNSIGNED = r"\d+" 

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

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

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

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

31_MATCH_WORD = ( 

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

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

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

35) 

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

37 

38 

39class Formatter: 

40 

41 _TOKENS = ( 

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

43 "(" 

44 "Mo|MM?M?M?" 

45 "|Do|DDDo|DD?D?D?|ddd?d?|do?" 

46 "|E{1,4}" 

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

48 "|YYYY|YY|Y" 

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

50 "|a|A" 

51 "|hh?|HH?|kk?" 

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

53 "|x|X" 

54 "|zz?|ZZ?" 

55 "|LTS|LT|LL?L?L?" 

56 ")" 

57 ) 

58 

59 _FORMAT_RE = re.compile(_TOKENS) 

60 

61 _FROM_FORMAT_RE = re.compile(r"(?<!\\\[)" + _TOKENS + r"(?!\\\])") 

62 

63 _LOCALIZABLE_TOKENS = { 

64 "Qo": None, 

65 "MMMM": "months.wide", 

66 "MMM": "months.abbreviated", 

67 "Mo": None, 

68 "DDDo": None, 

69 "Do": lambda locale: tuple( 

70 r"\d+{}".format(o) for o in locale.get("custom.ordinal").values() 

71 ), 

72 "dddd": "days.wide", 

73 "ddd": "days.abbreviated", 

74 "dd": "days.short", 

75 "do": None, 

76 "Wo": None, 

77 "wo": None, 

78 "A": lambda locale: ( 

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

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

81 ), 

82 "a": lambda locale: ( 

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

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

85 ), 

86 } 

87 

88 _TOKENS_RULES = { 

89 # Year 

90 "YYYY": lambda dt: "{:d}".format(dt.year), 

91 "YY": lambda dt: "{:d}".format(dt.year)[2:], 

92 "Y": lambda dt: "{:d}".format(dt.year), 

93 # Quarter 

94 "Q": lambda dt: "{:d}".format(dt.quarter), 

95 # Month 

96 "MM": lambda dt: "{:02d}".format(dt.month), 

97 "M": lambda dt: "{:d}".format(dt.month), 

98 # Day 

99 "DD": lambda dt: "{:02d}".format(dt.day), 

100 "D": lambda dt: "{:d}".format(dt.day), 

101 # Day of Year 

102 "DDDD": lambda dt: "{:03d}".format(dt.day_of_year), 

103 "DDD": lambda dt: "{:d}".format(dt.day_of_year), 

104 # Day of Week 

105 "d": lambda dt: "{:d}".format(dt.day_of_week), 

106 # Day of ISO Week 

107 "E": lambda dt: "{:d}".format(dt.isoweekday()), 

108 # Hour 

109 "HH": lambda dt: "{:02d}".format(dt.hour), 

110 "H": lambda dt: "{:d}".format(dt.hour), 

111 "hh": lambda dt: "{:02d}".format(dt.hour % 12 or 12), 

112 "h": lambda dt: "{:d}".format(dt.hour % 12 or 12), 

113 # Minute 

114 "mm": lambda dt: "{:02d}".format(dt.minute), 

115 "m": lambda dt: "{:d}".format(dt.minute), 

116 # Second 

117 "ss": lambda dt: "{:02d}".format(dt.second), 

118 "s": lambda dt: "{:d}".format(dt.second), 

119 # Fractional second 

120 "S": lambda dt: "{:01d}".format(dt.microsecond // 100000), 

121 "SS": lambda dt: "{:02d}".format(dt.microsecond // 10000), 

122 "SSS": lambda dt: "{:03d}".format(dt.microsecond // 1000), 

123 "SSSS": lambda dt: "{:04d}".format(dt.microsecond // 100), 

124 "SSSSS": lambda dt: "{:05d}".format(dt.microsecond // 10), 

125 "SSSSSS": lambda dt: "{:06d}".format(dt.microsecond), 

126 # Timestamp 

127 "X": lambda dt: "{:d}".format(dt.int_timestamp), 

128 "x": lambda dt: "{:d}".format(dt.int_timestamp * 1000 + dt.microsecond // 1000), 

129 # Timezone 

130 "zz": lambda dt: "{}".format(dt.tzname() if dt.tzinfo is not None else ""), 

131 "z": lambda dt: "{}".format(dt.timezone_name or ""), 

132 } 

133 

134 _DATE_FORMATS = { 

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

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

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

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

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

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

141 } 

142 

143 _DEFAULT_DATE_FORMATS = { 

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

145 "LT": "h:mm A", 

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

147 "LL": "MMMM D, YYYY", 

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

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

150 } 

151 

152 _REGEX_TOKENS = { 

153 "Y": _MATCH_SIGNED, 

154 "YY": (_MATCH_1_TO_2, _MATCH_2), 

155 "YYYY": (_MATCH_1_TO_4, _MATCH_4), 

156 "Q": _MATCH_1, 

157 "Qo": None, 

158 "M": _MATCH_1_TO_2, 

159 "MM": (_MATCH_1_TO_2, _MATCH_2), 

160 "MMM": _MATCH_WORD, 

161 "MMMM": _MATCH_WORD, 

162 "D": _MATCH_1_TO_2, 

163 "DD": (_MATCH_1_TO_2_LEFT_PAD, _MATCH_2), 

164 "DDD": _MATCH_1_TO_3, 

165 "DDDD": _MATCH_3, 

166 "dddd": _MATCH_WORD, 

167 "ddd": _MATCH_WORD, 

168 "dd": _MATCH_WORD, 

169 "d": _MATCH_1, 

170 "E": _MATCH_1, 

171 "Do": None, 

172 "H": _MATCH_1_TO_2, 

173 "HH": (_MATCH_1_TO_2, _MATCH_2), 

174 "h": _MATCH_1_TO_2, 

175 "hh": (_MATCH_1_TO_2, _MATCH_2), 

176 "m": _MATCH_1_TO_2, 

177 "mm": (_MATCH_1_TO_2, _MATCH_2), 

178 "s": _MATCH_1_TO_2, 

179 "ss": (_MATCH_1_TO_2, _MATCH_2), 

180 "S": (_MATCH_1_TO_3, _MATCH_1), 

181 "SS": (_MATCH_1_TO_3, _MATCH_2), 

182 "SSS": (_MATCH_1_TO_3, _MATCH_3), 

183 "SSSS": _MATCH_UNSIGNED, 

184 "SSSSS": _MATCH_UNSIGNED, 

185 "SSSSSS": _MATCH_UNSIGNED, 

186 "x": _MATCH_SIGNED, 

187 "X": _MATCH_TIMESTAMP, 

188 "ZZ": _MATCH_SHORT_OFFSET, 

189 "Z": _MATCH_OFFSET, 

190 "z": _MATCH_TIMEZONE, 

191 } 

192 

193 _PARSE_TOKENS = { 

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

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

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

197 "MMMM": lambda month: month, 

198 "MMM": lambda month: month, 

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

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

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

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

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

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

205 "dddd": lambda weekday: weekday, 

206 "ddd": lambda weekday: weekday, 

207 "dd": lambda weekday: weekday, 

208 "d": lambda weekday: int(weekday) % 7, 

209 "E": lambda weekday: int(weekday), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

224 "a": lambda meridiem: meridiem, 

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

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

227 "ZZ": str, 

228 "Z": str, 

229 "z": str, 

230 } 

231 

232 def format( 

233 self, dt, fmt, locale=None 

234 ): # type: (pendulum.DateTime, str, typing.Optional[typing.Union[str, Locale]]) -> str 

235 """ 

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

237 

238 :param dt: The instance to format 

239 :type dt: pendulum.DateTime 

240 

241 :param fmt: The format to use 

242 :type fmt: str 

243 

244 :param locale: The locale to use 

245 :type locale: str or Locale or None 

246 

247 :rtype: str 

248 """ 

249 if not locale: 

250 locale = pendulum.get_locale() 

251 

252 locale = Locale.load(locale) 

253 

254 result = self._FORMAT_RE.sub( 

255 lambda m: m.group(1) 

256 if m.group(1) 

257 else m.group(2) 

258 if m.group(2) 

259 else self._format_token(dt, m.group(3), locale), 

260 fmt, 

261 ) 

262 

263 return decode(result) 

264 

265 def _format_token( 

266 self, dt, token, locale 

267 ): # type: (pendulum.DateTime, str, Locale) -> str 

268 """ 

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

270 

271 :param dt: The instance to format 

272 :type dt: pendulum.DateTime 

273 

274 :param token: The token to use 

275 :type token: str 

276 

277 :param locale: The locale to use 

278 :type locale: Locale 

279 

280 :rtype: str 

281 """ 

282 if token in self._DATE_FORMATS: 

283 fmt = locale.get("custom.date_formats.{}".format(token)) 

284 if fmt is None: 

285 fmt = self._DEFAULT_DATE_FORMATS[token] 

286 

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

288 

289 if token in self._LOCALIZABLE_TOKENS: 

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

291 

292 if token in self._TOKENS_RULES: 

293 return self._TOKENS_RULES[token](dt) 

294 

295 # Timezone 

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

297 if dt.tzinfo is None: 

298 return "" 

299 

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

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

302 minutes = offset.total_seconds() / 60 

303 

304 if minutes >= 0: 

305 sign = "+" 

306 else: 

307 sign = "-" 

308 

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

310 

311 return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) 

312 

313 def _format_localizable_token( 

314 self, dt, token, locale 

315 ): # type: (pendulum.DateTime, str, Locale) -> str 

316 """ 

317 Formats a DateTime instance 

318 with a given localizable token and locale. 

319 

320 :param dt: The instance to format 

321 :type dt: pendulum.DateTime 

322 

323 :param token: The token to use 

324 :type token: str 

325 

326 :param locale: The locale to use 

327 :type locale: Locale 

328 

329 :rtype: str 

330 """ 

331 if token == "MMM": 

332 return locale.get("translations.months.abbreviated")[dt.month] 

333 elif token == "MMMM": 

334 return locale.get("translations.months.wide")[dt.month] 

335 elif token == "dd": 

336 return locale.get("translations.days.short")[dt.day_of_week] 

337 elif token == "ddd": 

338 return locale.get("translations.days.abbreviated")[dt.day_of_week] 

339 elif token == "dddd": 

340 return locale.get("translations.days.wide")[dt.day_of_week] 

341 elif token == "Do": 

342 return locale.ordinalize(dt.day) 

343 elif token == "do": 

344 return locale.ordinalize(dt.day_of_week) 

345 elif token == "Mo": 

346 return locale.ordinalize(dt.month) 

347 elif token == "Qo": 

348 return locale.ordinalize(dt.quarter) 

349 elif token == "wo": 

350 return locale.ordinalize(dt.week_of_year) 

351 elif token == "DDDo": 

352 return locale.ordinalize(dt.day_of_year) 

353 elif token == "A": 

354 key = "translations.day_periods" 

355 if dt.hour >= 12: 

356 key += ".pm" 

357 else: 

358 key += ".am" 

359 

360 return locale.get(key) 

361 else: 

362 return token 

363 

364 def parse( 

365 self, 

366 time, # type: str 

367 fmt, # type: str 

368 now, # type: pendulum.DateTime 

369 locale=None, # type: typing.Optional[str] 

370 ): # type: (...) -> typing.Dict[str, typing.Any] 

371 """ 

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

373 

374 :param time: The timestring 

375 :param fmt: The format 

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

377 :param locale: The locale to use 

378 

379 :return: The parsed elements 

380 """ 

381 escaped_fmt = re.escape(fmt) 

382 

383 tokens = self._FROM_FORMAT_RE.findall(escaped_fmt) 

384 if not tokens: 

385 return time 

386 

387 if not locale: 

388 locale = pendulum.get_locale() 

389 

390 locale = Locale.load(locale) 

391 

392 parsed = { 

393 "year": None, 

394 "month": None, 

395 "day": None, 

396 "hour": None, 

397 "minute": None, 

398 "second": None, 

399 "microsecond": None, 

400 "tz": None, 

401 "quarter": None, 

402 "day_of_week": None, 

403 "day_of_year": None, 

404 "meridiem": None, 

405 "timestamp": None, 

406 } 

407 

408 pattern = self._FROM_FORMAT_RE.sub( 

409 lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt 

410 ) 

411 

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

413 raise ValueError("String does not match format {}".format(fmt)) 

414 

415 re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time) 

416 

417 return self._check_parsed(parsed, now) 

418 

419 def _check_parsed( 

420 self, parsed, now 

421 ): # type: (typing.Dict[str, typing.Any], pendulum.DateTime) -> typing.Dict[str, typing.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 = { 

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("{}".format(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(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 = pendulum.parse( 

482 "{}-{:>03d}".format(validated["year"], parsed["day_of_year"]) 

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 validated["year"], 

491 validated["month"] or now.month, 

492 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 

518 if pm: 

519 validated["hour"] += 12 

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, m, parsed, locale, now 

543 ): # type: (typing.Match[str], typing.Dict[str, typing.Any], Locale, pendulum.DateTime) -> None 

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

545 if token in self._LOCALIZABLE_TOKENS: 

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

547 else: 

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

549 

550 def _get_parsed_value( 

551 self, token, value, parsed, now 

552 ): # type: (str, str, typing.Dict[str, typing.Any], pendulum.DateTime) -> None 

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

554 

555 if "Y" in token: 

556 if token == "YY": 

557 parsed_token = now.year // 100 * 100 + parsed_token 

558 

559 parsed["year"] = parsed_token 

560 elif "Q" == token: 

561 parsed["quarter"] = parsed_token 

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

563 parsed["month"] = parsed_token 

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

565 parsed["day_of_year"] = parsed_token 

566 elif "D" in token: 

567 parsed["day"] = parsed_token 

568 elif "H" in token: 

569 parsed["hour"] = parsed_token 

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

571 if parsed_token > 12: 

572 raise ValueError("Invalid date") 

573 

574 parsed["hour"] = parsed_token 

575 elif "m" in token: 

576 parsed["minute"] = parsed_token 

577 elif "s" in token: 

578 parsed["second"] = parsed_token 

579 elif "S" in token: 

580 parsed["microsecond"] = parsed_token 

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

582 parsed["day_of_week"] = parsed_token 

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

584 parsed["timestamp"] = parsed_token 

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

586 negative = True if value.startswith("-") else False 

587 tz = value[1:] 

588 if ":" not in tz: 

589 if len(tz) == 2: 

590 tz = "{}00".format(tz) 

591 

592 off_hour = tz[0:2] 

593 off_minute = tz[2:4] 

594 else: 

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

596 

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

598 

599 if negative: 

600 offset = -1 * offset 

601 

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

603 elif token == "z": 

604 # Full timezone 

605 if value not in pendulum.timezones: 

606 raise ValueError("Invalid date") 

607 

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

609 

610 def _get_parsed_locale_value( 

611 self, token, value, parsed, locale 

612 ): # type: (str, str, typing.Dict[str, typing.Any], Locale) -> None 

613 if token == "MMMM": 

614 unit = "month" 

615 match = "months.wide" 

616 elif token == "MMM": 

617 unit = "month" 

618 match = "months.abbreviated" 

619 elif token == "Do": 

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

621 

622 return 

623 elif token == "dddd": 

624 unit = "day_of_week" 

625 match = "days.wide" 

626 elif token == "ddd": 

627 unit = "day_of_week" 

628 match = "days.abbreviated" 

629 elif token == "dd": 

630 unit = "day_of_week" 

631 match = "days.short" 

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

633 valid_values = [ 

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

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

636 ] 

637 

638 if token == "a": 

639 value = value.lower() 

640 valid_values = list(map(lambda x: x.lower(), valid_values)) 

641 

642 if value not in valid_values: 

643 raise ValueError("Invalid date") 

644 

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

646 

647 return 

648 else: 

649 raise ValueError('Invalid token "{}"'.format(token)) 

650 

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

652 if value is None: 

653 raise ValueError("Invalid date") 

654 

655 def _replace_tokens(self, token, locale): # type: (str, Locale) -> str 

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

657 return token[1:-1] 

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

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

660 return "" 

661 

662 return token 

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

664 raise ValueError("Unsupported token: {}".format(token)) 

665 

666 if token in self._LOCALIZABLE_TOKENS: 

667 values = self._LOCALIZABLE_TOKENS[token] 

668 if callable(values): 

669 candidates = values(locale) 

670 else: 

671 candidates = tuple( 

672 locale.translation(self._LOCALIZABLE_TOKENS[token]).values() 

673 ) 

674 else: 

675 candidates = self._REGEX_TOKENS[token] 

676 

677 if not candidates: 

678 raise ValueError("Unsupported token: {}".format(token)) 

679 

680 if not isinstance(candidates, tuple): 

681 candidates = (candidates,) 

682 

683 pattern = "(?P<{}>{})".format(token, "|".join([decode(p) for p in candidates])) 

684 

685 return pattern