Skip to content

Commit abf7130

Browse files
authored
Fix incorrect date parsing for multi-word number representations (#1280)
Fix incorrect date parsing for multi-word number representations Close #1260
1 parent f69e9b2 commit abf7130

File tree

4 files changed

+223
-41
lines changed

4 files changed

+223
-41
lines changed

dateparser/data/date_translation_data/ru.py

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -340,73 +340,91 @@
340340
],
341341
"simplifications": [
342342
{
343-
"од(на|ну|ни|ной|ин)": "1"
343+
"од(ин|на|ну|ни|ной|ною|но|ного|ному|ним|нем)": "1"
344344
},
345345
{
346-
"дв(а|е|ое|ух)": "2"
346+
"перв(ой|ого|ому|ым|ом|ая|ую|ое|ые|ых|ыми)": "1"
347347
},
348348
{
349-
"пар[ауы]": "2"
349+
"дв(а|е|ух|ум|умя|ое)": "2"
350350
},
351351
{
352-
"три": "3"
352+
"пар(а|ы|е|у|ой|ою|ам|ами|ах)": "2"
353353
},
354354
{
355-
"четыре": "4"
355+
"втор(ой|ого|ому|ым|ом|ая|ую|ое|ые|ых|ыми)": "2"
356356
},
357357
{
358-
"пять": "5"
358+
"тр(и|ёх|ем|ём|емя|етье)": "3"
359359
},
360360
{
361-
"шесть": "6"
361+
"трети(й|его|ему|им|ем|я|ей|ею|ую|е|и|их|ими)": "3"
362362
},
363363
{
364-
"семь": "7"
364+
"четыр(е|ёх|ем|ём|ьмя)": "4"
365365
},
366366
{
367-
"восемь": "8"
367+
"четвёрт(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми)": "4"
368368
},
369369
{
370-
"девять": "9"
370+
"четверт(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми)": "4"
371371
},
372372
{
373-
"десять": "10"
373+
"пят(ь|и|ью)": "5"
374374
},
375375
{
376-
"одиннадцать": "11"
376+
"пят(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми)": "5"
377377
},
378378
{
379-
"двенадцать": "12"
379+
"шест(ь|и|ью)": "6"
380380
},
381381
{
382-
"пятнадцать": "15"
382+
"шест(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми)": "6"
383383
},
384384
{
385-
"двадцать": "20"
385+
"сем(ь|и|ью)": "7"
386386
},
387387
{
388-
"тридцать": "30"
388+
"седьм(ой|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми)": "7"
389389
},
390390
{
391-
"сорок": "40"
391+
"восьм(и|ью|ьею)|восем(ь|ью)": "8"
392392
},
393393
{
394-
"пятьдесят": "50"
394+
"восьм(ой|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми)": "8"
395395
},
396396
{
397-
"несколько секунд": "44 секунды"
397+
"девят(ь|и|ью)": "9"
398398
},
399399
{
400-
"полчаса": "30 минут"
400+
"девят(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми)": "9"
401401
},
402402
{
403-
"полгода": "6 месяцев"
403+
"десять": "10"
404404
},
405405
{
406-
"полтора часа": "90 минут"
406+
"одиннадцать": "11"
407407
},
408408
{
409-
"полтора года": "18 месяцев"
409+
"двенадцать": "12"
410+
},
411+
{
412+
"пятнадцать": "15"
413+
},
414+
{
415+
"двадцат(ь|ое)": "20"
416+
},
417+
{
418+
"тридцат(ь|ое)": "30"
419+
},
420+
{
421+
"соро(к|ка|ковое)": "40"
422+
},
423+
{
424+
"пятьдесят": "50"
425+
},
426+
{
427+
"пятидесятое": "50"
410428
},
411429
{
412430
"((?<=(через|спустя|в течение)\\s+)секунд[уы]|(?<=[^\\d]\\s+|^)секунду(?=(\\s+назад)))": "1 секунду"
@@ -426,12 +444,27 @@
426444
{
427445
"((?<=(через|спустя|в течение)\\s+)недел[юи]|(?<=[^\\d]\\s+|^)неделю(?=(\\s+назад)))": "1 неделю"
428446
},
447+
{
448+
"полгода": "6 месяцев"
449+
},
429450
{
430451
"((?<=(через|спустя|в течение)\\s+)месяца?|(?<=[^\\d]\\s+|^)месяц(?=(\\s+назад)))": "1 месяц"
431452
},
432453
{
433454
"((?<=(через|спустя|в течение)\\s+)года?|(?<=[^\\d]\\s+|^)год(?=(\\s+назад)))": "1 год"
434455
},
456+
{
457+
"полтора года": "18 месяцев"
458+
},
459+
{
460+
"полчаса": "30 минут"
461+
},
462+
{
463+
"несколько секунд": "44 секунды"
464+
},
465+
{
466+
"полтора часа": "90 минут"
467+
},
435468
{
436469
"(\\d{3,}1)\\s*год\\s*$": "\\1"
437470
},

dateparser/languages/locale.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,39 @@ def _token_with_digits_is_ok(self, token):
426426
def _simplify(self, date_string, settings=None):
427427
date_string = date_string.lower()
428428
simplifications = self._get_simplifications(settings=settings)
429+
430+
if self.info.get("name") == "ru":
431+
date_string = self._process_russian_compound_ordinals(
432+
date_string, simplifications
433+
)
434+
else:
435+
date_string = self._apply_simplifications(date_string, simplifications)
436+
437+
return date_string
438+
439+
def _apply_simplifications(self, date_string, simplifications):
429440
for simplification in simplifications:
430441
pattern, replacement = list(simplification.items())[0]
431442
date_string = pattern.sub(replacement, date_string).lower()
432443
return date_string
433444

445+
def _process_russian_compound_ordinals(self, date_string, simplifications):
446+
"""Process Russian compound ordinals mathematically (двадцать + первое = 21)."""
447+
date_string = self._apply_simplifications(date_string, simplifications)
448+
449+
def replace_number_pairs(match):
450+
first_num = int(match.group(1))
451+
second_num = int(match.group(2))
452+
result = first_num + second_num
453+
if 1 <= result <= 31 and first_num in [20, 30] and 1 <= second_num <= 9:
454+
return str(result)
455+
return match.group(0)
456+
457+
number_pair_pattern = r"\b(\d+)\s+(\d+)\b"
458+
date_string = re.sub(number_pair_pattern, replace_number_pairs, date_string)
459+
460+
return date_string
461+
434462
def _get_simplifications(self, settings=None):
435463
no_word_spacing = eval(self.info.get("no_word_spacing", "False"))
436464
if settings.NORMALIZE:

dateparser_data/supplementary_language_data/date_translation_data/ru.yaml

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,36 +72,47 @@ relative-type:
7272
- послепослезавтра
7373

7474
simplifications:
75-
- од(на|ну|ни|ной|ин): '1'
76-
- дв(а|е|ое|ух): '2'
77-
- пар[ауы]: '2'
78-
- три: '3'
79-
- четыре: '4'
80-
- пять: '5'
81-
- шесть: '6'
82-
- семь: '7'
83-
- восемь: '8'
84-
- девять: '9'
75+
- од(ин|на|ну|ни|ной|ною|но|ного|ному|ним|нем): '1'
76+
- перв(ой|ого|ому|ым|ом|ая|ую|ое|ые|ых|ыми): '1'
77+
- дв(а|е|ух|ум|умя|ое): '2'
78+
- пар(а|ы|е|у|ой|ою|ам|ами|ах): '2'
79+
- втор(ой|ого|ому|ым|ом|ая|ую|ое|ые|ых|ыми): '2'
80+
- тр(и|ёх|ем|ём|емя|етье): '3'
81+
- трети(й|его|ему|им|ем|я|ей|ею|ую|е|и|их|ими): '3'
82+
- четыр(е|ёх|ем|ём|ьмя): '4'
83+
- четвёрт(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми): '4'
84+
- четверт(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми): '4'
85+
- пят(ь|и|ью): '5'
86+
- пят(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми): '5'
87+
- шест(ь|и|ью): '6'
88+
- шест(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми): '6'
89+
- сем(ь|и|ью): '7'
90+
- седьм(ой|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми): '7'
91+
- восьм(и|ью|ьею)|восем(ь|ью): '8'
92+
- восьм(ой|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми): '8'
93+
- девят(ь|и|ью): '9'
94+
- девят(ый|ого|ому|ым|ом|ая|ой|ою|ую|ое|ые|ых|ыми): '9'
8595
- десять: '10'
8696
- одиннадцать: '11'
8797
- двенадцать: '12'
8898
- пятнадцать: '15'
89-
- двадцать: '20'
90-
- тридцать: '30'
91-
- сорок: '40'
99+
- двадцат(ь|ое): '20'
100+
- тридцат(ь|ое): '30'
101+
- соро(к|ка|ковое): '40'
92102
- пятьдесят: '50'
93-
- несколько секунд: 44 секунды
94-
- полчаса: 30 минут
95-
- полгода: 6 месяцев
96-
- полтора часа: 90 минут
97-
- полтора года: 18 месяцев
103+
- пятидесятое: '50'
98104
- ((?<=(через|спустя|в течение)\s+)секунд[уы]|(?<=[^\d]\s+|^)секунду(?=(\s+назад))): 1 секунду
99105
- ((?<=(через|спустя|в течение)\s+)минут[уы]|(?<=[^\d]\s+|^)минуту(?=(\s+назад))): 1 минуту
100106
- ((?<=(через|спустя|в течение)\s+)часа?|(?<=[^\d]\s+|^)час(?=(\s+назад))): 1 час
101107
- ((?<=(через|спустя|в течение)\s+)(день|дня)|(?<=[^\d]\s+|^)день(?=(\s+назад))): 1 день
102108
- ((?<=(через|спустя|в течение)\s+)сут(ки|ок)|(?<=[^\d]\s+|^)сутки(?=(\s+назад))): 1 сутки
103109
- ((?<=(через|спустя|в течение)\s+)недел[юи]|(?<=[^\d]\s+|^)неделю(?=(\s+назад))): 1 неделю
110+
- полгода: 6 месяцев
104111
- ((?<=(через|спустя|в течение)\s+)месяца?|(?<=[^\d]\s+|^)месяц(?=(\s+назад))): 1 месяц
105112
- ((?<=(через|спустя|в течение)\s+)года?|(?<=[^\d]\s+|^)год(?=(\s+назад))): 1 год
113+
- полтора года: 18 месяцев
114+
- полчаса: 30 минут
115+
- несколько секунд: 44 секунды
116+
- полтора часа: 90 минут
106117
- (\d{3,}1)\s*год\s*$: \1
107118
- (\d*[02-9])\s*год\b: \1

tests/test_search.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,3 +1103,113 @@ def test_search_dates_with_prepositions(self):
11031103
("30 апреля", datetime.datetime(2025, 4, 30, 0, 0), "ru"),
11041104
]
11051105
assert result == expected
1106+
1107+
@parameterized.expand(
1108+
[
1109+
param(
1110+
text="Ужасное событие произошло в тот день. Двадцатое февраля. Вспоминаю тот день с ужасом.",
1111+
expected_text="Двадцатое февраля",
1112+
expected_day=20,
1113+
expected_month=2,
1114+
description="20th February",
1115+
),
1116+
param(
1117+
text="Ужасное событие произошло в тот день. Двадцать первое февраля. Вспоминаю тот день с ужасом.",
1118+
expected_text="Двадцать первое февраля",
1119+
expected_day=21,
1120+
expected_month=2,
1121+
description="21st February",
1122+
),
1123+
param(
1124+
text="Ужасное событие произошло в тот день. Двадцать второе февраля. Вспоминаю тот день с ужасом.",
1125+
expected_text="Двадцать второе февраля",
1126+
expected_day=22,
1127+
expected_month=2,
1128+
description="22nd February",
1129+
),
1130+
param(
1131+
text="Ужасное событие произошло в тот день. Двадцать третье февраля. Вспоминаю тот день с ужасом.",
1132+
expected_text="Двадцать третье февраля",
1133+
expected_day=23,
1134+
expected_month=2,
1135+
description="23rd February",
1136+
),
1137+
param(
1138+
text="Ужасное событие произошло в тот день. Двадцать четвёртое февраля. Вспоминаю тот день с ужасом.",
1139+
expected_text="Двадцать четвёртое февраля",
1140+
expected_day=24,
1141+
expected_month=2,
1142+
description="24th February (with ё)",
1143+
),
1144+
param(
1145+
text="Ужасное событие произошло в тот день. Двадцать четвертое февраля. Вспоминаю тот день с ужасом.",
1146+
expected_text="Двадцать четвертое февраля",
1147+
expected_day=24,
1148+
expected_month=2,
1149+
description="24th February (without ё)",
1150+
),
1151+
param(
1152+
text="Ужасное событие произошло в тот день. Двадцать пятое марта. Вспоминаю тот день с ужасом.",
1153+
expected_text="Двадцать пятое марта",
1154+
expected_day=25,
1155+
expected_month=3,
1156+
description="25th March",
1157+
),
1158+
param(
1159+
text="Ужасное событие произошло в тот день. Двадцать шестое марта. Вспоминаю тот день с ужасом.",
1160+
expected_text="Двадцать шестое марта",
1161+
expected_day=26,
1162+
expected_month=3,
1163+
description="26th March",
1164+
),
1165+
param(
1166+
text="Ужасное событие произошло в тот день. Двадцать седьмое марта. Вспоминаю тот день с ужасом.",
1167+
expected_text="Двадцать седьмое марта",
1168+
expected_day=27,
1169+
expected_month=3,
1170+
description="27th March",
1171+
),
1172+
param(
1173+
text="Ужасное событие произошло в тот день. Двадцать восьмое марта. Вспоминаю тот день с ужасом.",
1174+
expected_text="Двадцать восьмое марта",
1175+
expected_day=28,
1176+
expected_month=3,
1177+
description="28th March",
1178+
),
1179+
param(
1180+
text="Ужасное событие произошло в тот день. Двадцать девятое марта. Вспоминаю тот день с ужасом.",
1181+
expected_text="Двадцать девятое марта",
1182+
expected_day=29,
1183+
expected_month=3,
1184+
description="29th March",
1185+
),
1186+
param(
1187+
text="Ужасное событие произошло в тот день. Тридцатое марта. Вспоминаю тот день с ужасом.",
1188+
expected_text="Тридцатое марта",
1189+
expected_day=30,
1190+
expected_month=3,
1191+
description="30th March",
1192+
),
1193+
param(
1194+
text="Ужасное событие произошло в тот день. Тридцать первое марта. Вспоминаю тот день с ужасом.",
1195+
expected_text="Тридцать первое марта",
1196+
expected_day=31,
1197+
expected_month=3,
1198+
description="31st March",
1199+
),
1200+
]
1201+
)
1202+
def test_search_dates_multi_word_expression(
1203+
self, text, expected_text, expected_day, expected_month, description
1204+
):
1205+
"""Test parsing of multi-word date expressions in Russian."""
1206+
result = search_dates(text, languages=["ru"])
1207+
expected = [
1208+
(
1209+
expected_text,
1210+
datetime.datetime(
1211+
datetime.datetime.now().year, expected_month, expected_day, 0, 0
1212+
),
1213+
)
1214+
]
1215+
self.assertEqual(result, expected)

0 commit comments

Comments
 (0)