From 35e7f61f72a9e26ab88ef697b2f0caa128aca72a Mon Sep 17 00:00:00 2001 From: Prashant Sharma <31796326+gutsytechster@users.noreply.github.com> Date: Thu, 5 Feb 2026 22:00:52 +0530 Subject: [PATCH 1/5] Use different timezones to run CI tests (#1259) --- .github/workflows/main.yml | 10 +++++++++- tests/test_search.py | 2 +- tox.ini | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b17e70667..cb397b116 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,12 @@ jobs: toxenv: docs - python-version: "3.14" toxenv: twinecheck + - python-version: "3.13" + toxenv: py + timezone: "Pacific/Auckland" + - python-version: "3.13" + toxenv: py + timezone: "Pacific/Fiji" steps: - uses: actions/checkout@v3 - name: 'Set up Python ${{ matrix.python-version }}' @@ -43,7 +49,9 @@ jobs: python -m pip install --upgrade pip pip install tox - name: Run tests - run: tox -e ${{ matrix.toxenv || 'py' }} + run: | + TZ=${{ matrix.timezone || 'UTC' }} echo "Running tests with timezone: $TZ" + TZ=${{ matrix.timezone || 'UTC' }} tox -e ${{ matrix.toxenv || 'py' }} - name: Upload coverage.xml to codecov uses: codecov/codecov-action@v5 with: diff --git a/tests/test_search.py b/tests/test_search.py index 7c3b3322e..47e380cf7 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -11,7 +11,7 @@ from dateparser_data.settings import default_parsers from tests import BaseTestCase -today = datetime.datetime.today() +today = datetime.datetime.now(tz=pytz.timezone("UTC")) class TestTranslateSearch(BaseTestCase): diff --git a/tox.ini b/tox.ini index 05b10493c..ec567268b 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ deps = atheris; python_version < '3.12' commands = pytest --cov=dateparser --cov-report=xml {posargs: tests} +passenv = TZ [testenv:all] basepython = python3.14 From 0f0419944c000f59f214d68f10a3f6488a0ef67e Mon Sep 17 00:00:00 2001 From: branislavroljic <58853003+branislavroljic@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:32:00 +0100 Subject: [PATCH 2/5] Add Bosnian Cyrillic (ijekavica) date translations (#1293) --- .../data/date_translation_data/bs-Cyrl.py | 132 +++++++++++++----- .../date_translation_data/bs-Cyrl.yaml | 113 +++++++++++++++ tests/test_languages.py | 22 +++ 3 files changed, 235 insertions(+), 32 deletions(-) create mode 100644 dateparser_data/supplementary_language_data/date_translation_data/bs-Cyrl.yaml diff --git a/dateparser/data/date_translation_data/bs-Cyrl.py b/dateparser/data/date_translation_data/bs-Cyrl.py index c07a696f8..0b3951a41 100644 --- a/dateparser/data/date_translation_data/bs-Cyrl.py +++ b/dateparser/data/date_translation_data/bs-Cyrl.py @@ -50,7 +50,8 @@ ], "monday": [ "пон", - "понедељак" + "понедељак", + "понедјељак" ], "tuesday": [ "уто", @@ -74,53 +75,77 @@ ], "sunday": [ "нед", - "недеља" + "недеља", + "недјеља" ], "am": [ - "пре подне" + "пре подне", + "пријеподне" ], "pm": [ - "поподне" + "поподне", + "послијеподне" ], "year": [ - "година" + "година", + "г", + "год", + "годинa" ], "month": [ - "месец" + "месец", + "мј", + "мјесец", + "мјесеци", + "мјесеца" ], "week": [ - "недеља" + "недеља", + "сед", + "седмица", + "седмице" ], "day": [ "дан" ], "hour": [ - "час" + "час", + "сат" ], "minute": [ - "минут" + "минут", + "мин", + "минута" ], "second": [ - "секунд" + "секунд", + "с", + "сек", + "секунда" ], "relative-type": { "0 day ago": [ "данас" ], "0 hour ago": [ - "this hour" + "this hour", + "овај сат" ], "0 minute ago": [ - "this minute" + "this minute", + "ова минута" ], "0 month ago": [ - "овог месеца" + "овог месеца", + "овај мјесец" ], "0 second ago": [ - "now" + "now", + "сада" ], "0 week ago": [ - "ове недеље" + "ове недеље", + "ове седмице" ], "0 year ago": [ "ове године" @@ -129,10 +154,12 @@ "јуче" ], "1 month ago": [ - "прошлог месеца" + "прошлог месеца", + "прошли мјесец" ], "1 week ago": [ - "прошле недеље" + "прошле недеље", + "прошле седмице" ], "1 year ago": [ "прошле године" @@ -141,43 +168,68 @@ "сутра" ], "in 1 month": [ - "следећег месеца" + "следећег месеца", + "сљедећи мјесец" ], "in 1 week": [ - "следеће недеље" + "следеће недеље", + "сљедеће седмице" ], "in 1 year": [ - "следеће године" + "следеће године", + "сљедеће године" ] }, "relative-type-regex": { "\\1 day ago": [ "пре (\\d+[.,]?\\d*) дан", - "пре (\\d+[.,]?\\d*) дана" + "пре (\\d+[.,]?\\d*) дана", + "прије (\\d+[.,]?\\d*) дан", + "прије (\\d+[.,]?\\d*) дана" ], "\\1 hour ago": [ "пре (\\d+[.,]?\\d*) сат", - "пре (\\d+[.,]?\\d*) сати" + "пре (\\d+[.,]?\\d*) сати", + "прије (\\d+[.,]?\\d*) сат", + "прије (\\d+[.,]?\\d*) сати" ], "\\1 minute ago": [ "пре (\\d+[.,]?\\d*) минут", - "пре (\\d+[.,]?\\d*) минута" + "пре (\\d+[.,]?\\d*) минута", + "прије (\\d+[.,]?\\d*) мин", + "прије (\\d+[.,]?\\d*) минута", + "прије (\\d+[.,]?\\d*) минуту" ], "\\1 month ago": [ "пре (\\d+[.,]?\\d*) месец", - "пре (\\d+[.,]?\\d*) месеци" + "пре (\\d+[.,]?\\d*) месеци", + "прије (\\d+[.,]?\\d*) мј", + "прије (\\d+[.,]?\\d*) мјесец", + "прије (\\d+[.,]?\\d*) мјесеца", + "прије (\\d+[.,]?\\d*) мјесеци" ], "\\1 second ago": [ "пре (\\d+[.,]?\\d*) секунд", - "пре (\\d+[.,]?\\d*) секунди" + "пре (\\d+[.,]?\\d*) секунди", + "прије (\\d+[.,]?\\d*) сек", + "прије (\\d+[.,]?\\d*) секунда", + "прије (\\d+[.,]?\\d*) секунду" ], "\\1 week ago": [ "пре (\\d+[.,]?\\d*) недеља", - "пре (\\d+[.,]?\\d*) недељу" + "пре (\\d+[.,]?\\d*) недељу", + "прије (\\d+[.,]?\\d*) сед", + "прије (\\d+[.,]?\\d*) седмица", + "прије (\\d+[.,]?\\d*) седмице", + "прије (\\d+[.,]?\\d*) седмицу" ], "\\1 year ago": [ "пре (\\d+[.,]?\\d*) година", - "пре (\\d+[.,]?\\d*) годину" + "пре (\\d+[.,]?\\d*) годину", + "прије (\\d+[.,]?\\d*) г", + "прије (\\d+[.,]?\\d*) год", + "прије (\\d+[.,]?\\d*) година", + "прије (\\d+[.,]?\\d*) годину" ], "in \\1 day": [ "за (\\d+[.,]?\\d*) дан", @@ -189,23 +241,39 @@ ], "in \\1 minute": [ "за (\\d+[.,]?\\d*) минут", - "за (\\d+[.,]?\\d*) минута" + "за (\\d+[.,]?\\d*) минута", + "за (\\d+[.,]?\\d*) мин", + "за (\\d+[.,]?\\d*) минута", + "за (\\d+[.,]?\\d*) минуту" ], "in \\1 month": [ "за (\\d+[.,]?\\d*) месец", - "за (\\d+[.,]?\\d*) месеци" + "за (\\d+[.,]?\\d*) месеци", + "за (\\d+[.,]?\\d*) мј", + "за (\\d+[.,]?\\d*) мјесец", + "за (\\d+[.,]?\\d*) мјесеца", + "за (\\d+[.,]?\\d*) мјесеци" ], "in \\1 second": [ "за (\\d+[.,]?\\d*) секунд", - "за (\\d+[.,]?\\d*) секунди" + "за (\\d+[.,]?\\d*) секунди", + "за (\\d+[.,]?\\d*) сек", + "за (\\d+[.,]?\\d*) секунда", + "за (\\d+[.,]?\\d*) секунду" ], "in \\1 week": [ "за (\\d+[.,]?\\d*) недеља", - "за (\\d+[.,]?\\d*) недељу" + "за (\\d+[.,]?\\d*) недељу", + "за (\\d+[.,]?\\d*) сед", + "за (\\d+[.,]?\\d*) седмица", + "за (\\d+[.,]?\\d*) седмицу", + "за (\\d+[.,]?\\d*) седмице" ], "in \\1 year": [ "за (\\d+[.,]?\\d*) година", - "за (\\d+[.,]?\\d*) годину" + "за (\\d+[.,]?\\d*) годину", + "за (\\d+[.,]?\\d*) г", + "за (\\d+[.,]?\\d*) год" ] }, "locale_specific": {}, diff --git a/dateparser_data/supplementary_language_data/date_translation_data/bs-Cyrl.yaml b/dateparser_data/supplementary_language_data/date_translation_data/bs-Cyrl.yaml new file mode 100644 index 000000000..9953707f7 --- /dev/null +++ b/dateparser_data/supplementary_language_data/date_translation_data/bs-Cyrl.yaml @@ -0,0 +1,113 @@ +am: + - пријеподне +pm: + - послијеподне + +monday: + - понедјељак + +sunday: + - недјеља + +year: + - г + - год + - годинa + +month: + - мј + - мјесец + - мјесеци + - мјесеца + +week: + - сед + - седмица + - седмице + +hour: + - сат + +minute: + - мин + - минута + +second: + - с + - сек + - секунда + +relative-type: + 0 hour ago: + - овај сат + 0 minute ago: + - ова минута + 0 month ago: + - овај мјесец + 0 second ago: + - сада + 0 week ago: + - ове седмице + 1 month ago: + - прошли мјесец + 1 week ago: + - прошле седмице + in 1 month: + - сљедећи мјесец + in 1 week: + - сљедеће седмице + in 1 year: + - сљедеће године + +relative-type-regex: + \1 day ago: + - прије (\d+[.,]?\d*) дан + - прије (\d+[.,]?\d*) дана + \1 hour ago: + - прије (\d+[.,]?\d*) сат + - прије (\d+[.,]?\d*) сати + \1 minute ago: + - прије (\d+[.,]?\d*) мин + - прије (\d+[.,]?\d*) минута + - прије (\d+[.,]?\d*) минуту + \1 month ago: + - прије (\d+[.,]?\d*) мј + - прије (\d+[.,]?\d*) мјесец + - прије (\d+[.,]?\d*) мјесеца + - прије (\d+[.,]?\d*) мјесеци + \1 second ago: + - прије (\d+[.,]?\d*) сек + - прије (\d+[.,]?\d*) секунда + - прије (\d+[.,]?\d*) секунду + \1 week ago: + - прије (\d+[.,]?\d*) сед + - прије (\d+[.,]?\d*) седмица + - прије (\d+[.,]?\d*) седмице + - прије (\d+[.,]?\d*) седмицу + \1 year ago: + - прије (\d+[.,]?\d*) г + - прије (\d+[.,]?\d*) год + - прије (\d+[.,]?\d*) година + - прије (\d+[.,]?\d*) годину + + in \1 minute: + - за (\d+[.,]?\d*) мин + - за (\d+[.,]?\d*) минута + - за (\d+[.,]?\d*) минуту + in \1 month: + - за (\d+[.,]?\d*) мј + - за (\d+[.,]?\d*) мјесец + - за (\d+[.,]?\d*) мјесеца + - за (\d+[.,]?\d*) мјесеци + in \1 second: + - за (\d+[.,]?\d*) сек + - за (\d+[.,]?\d*) секунда + - за (\d+[.,]?\d*) секунду + in \1 week: + - за (\d+[.,]?\d*) сед + - за (\d+[.,]?\d*) седмица + - за (\d+[.,]?\d*) седмицу + - за (\d+[.,]?\d*) седмице + in \1 year: + - за (\d+[.,]?\d*) г + - за (\d+[.,]?\d*) год diff --git a/tests/test_languages.py b/tests/test_languages.py index 1ba280794..cffc8e329 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -334,6 +334,17 @@ def setUp(self): # bs-Cyrl param("bs-Cyrl", "2 септембар 2000, четвртак", "2 september 2000 thursday"), param("bs-Cyrl", "1 јули 1987 9:25 поподне", "1 july 1987 9:25 pm"), + param("bs-Cyrl", "1 јули 1987 9:25 послијеподне", "1 july 1987 9:25 pm"), + param( + "bs-Cyrl", + "понедјељак, 1 јули 1987 9:25 пријеподне", + "monday 1 july 1987 9:25 am", + ), + param( + "bs-Cyrl", + "недјеља, 2 септембар 2000 7:00 послијеподне", + "sunday 2 september 2000 7:00 pm", + ), # bs-Latn param("bs-Latn", "23 septembar 1879, petak", "23 september 1879 friday"), param( @@ -1396,6 +1407,17 @@ def test_translation(self, shortname, datetime_string, expected_translation): param("bs-Cyrl", "следећег месеца", "in 1 month"), param("bs-Cyrl", "прошле године 10:05 пре подне", "1 year ago 10:05 am"), param("bs-Cyrl", "пре 28 недеља", "28 week ago"), + param("bs-Cyrl", "сљедећи мјесец", "in 1 month"), + param("bs-Cyrl", "сљедеће седмице", "in 1 week"), + param("bs-Cyrl", "сљедеће године", "in 1 year"), + param("bs-Cyrl", "прије 4 мјесеца", "4 month ago"), + param("bs-Cyrl", "прије 2 седмице", "2 week ago"), + param("bs-Cyrl", "прошли мјесец", "1 month ago"), + param("bs-Cyrl", "прошле седмице", "1 week ago"), + param("bs-Cyrl", "овај мјесец", "0 month ago"), + param("bs-Cyrl", "ове седмице", "0 week ago"), + param("bs-Cyrl", "за 2 мјесеца", "in 2 month"), + param("bs-Cyrl", "за 3 седмице", "in 3 week"), # bs-Latn param("bs-Latn", "sljedeće godine", "in 1 year"), param("bs-Latn", "prije 4 mjeseci", "4 month ago"), From 169c4ce94b90095c07c83dd497f8f80f74cc869b Mon Sep 17 00:00:00 2001 From: Gabriel Nyman <1233346+gnyman@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:34:26 +0200 Subject: [PATCH 3/5] New browser based demo (#1306) The demo has been broken for a long time #1173 and #1188 I played around with GitHub Spark throw and put together a in-browser demo, and then used claude to decouple it from Spark. It doesn't require any python backend but runs it in the browser with Pyodide, thus should be easier and cheaper to host. It could even be brought into this repository to make it a official demo. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 31a0b6add..6106107f7 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,7 @@ Online demo ----------- Do you want to try it out without installing any dependency? Now you can test -it quickly by visiting `this online demo `__! +it quickly by visiting `this online demo `__! From 6ca550f631baa7f1de0d3f6dbc21bfae55158523 Mon Sep 17 00:00:00 2001 From: Justin Keogh Date: Thu, 5 Feb 2026 09:35:13 -0700 Subject: [PATCH 4/5] Allow 'N {interval} from now' (#502) (#1271) --- dateparser/data/date_translation_data/en.py | 1 + .../date_translation_data/en.yaml | 2 +- tests/test_freshness_date_parser.py | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/dateparser/data/date_translation_data/en.py b/dateparser/data/date_translation_data/en.py index ffa7a6de4..042a896c2 100644 --- a/dateparser/data/date_translation_data/en.py +++ b/dateparser/data/date_translation_data/en.py @@ -756,6 +756,7 @@ "and", "at", "by", + "from", "just", "m", "nd", diff --git a/dateparser_data/supplementary_language_data/date_translation_data/en.yaml b/dateparser_data/supplementary_language_data/date_translation_data/en.yaml index 9e888f388..dab056b1d 100644 --- a/dateparser_data/supplementary_language_data/date_translation_data/en.yaml +++ b/dateparser_data/supplementary_language_data/date_translation_data/en.yaml @@ -1,4 +1,4 @@ -skip: ["about", "ad", "and", "at", "by", "just", "m", "nd", "of", "on", "rd", "st", "th", "the"] +skip: ["about", "ad", "and", "at", "by", "from", "just", "m", "nd", "of", "on", "rd", "st", "th", "the"] pertain: ["of"] sentence_splitter_group : 1 diff --git a/tests/test_freshness_date_parser.py b/tests/test_freshness_date_parser.py index 0ba2d60fe..5f790dc66 100644 --- a/tests/test_freshness_date_parser.py +++ b/tests/test_freshness_date_parser.py @@ -1808,6 +1808,22 @@ def test_normalized_relative_dates(self, date_string, ago, period): param("3 hours later", in_future={"hours": 3}, period="day"), param("4 minutes later", in_future={"minutes": 4}, period="day"), param("5 seconds later", in_future={"seconds": 5}, period="day"), + # from now + param("7 years from now", in_future={"years": 7}, period="year"), + param("6 months from now", in_future={"months": 6}, period="month"), + param("5 weeks from now", in_future={"weeks": 5}, period="week"), + param("4 days from now", in_future={"days": 4}, period="day"), + param("3 hours from now", in_future={"hours": 3}, period="day"), + param("2 minutes from now", in_future={"minutes": 2}, period="day"), + param("1 second from now", in_future={"seconds": 1}, period="day"), + param("five years from now", in_future={"years": 5}, period="year"), + param("a year from now", in_future={"years": 1}, period="year"), + param("an hour from now", in_future={"hours": 1}, period="day"), + param( + "1 year 2 months from now", + in_future={"years": 1, "months": 2}, + period="month", + ), # Fractional units param("in 2.5 hours", in_future={"hours": 2.5}, period="day"), param("in 10.75 minutes", in_future={"minutes": 10.75}, period="day"), From cd5f226454e0ed3fe93164e7eff55b00f57e57c7 Mon Sep 17 00:00:00 2001 From: Mrinal Jain Date: Thu, 5 Feb 2026 12:19:38 -0500 Subject: [PATCH 5/5] Honor `REQUIRE_PARTS` for ambiguous month-number inputs by retrying year-biased `DATE_ORDER` (#1298) --- dateparser/date.py | 56 ++++++++++++++++++++++++++++------------- tests/test_clean_api.py | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 17 deletions(-) diff --git a/dateparser/date.py b/dateparser/date.py index e23444720..12b05d4ba 100644 --- a/dateparser/date.py +++ b/dateparser/date.py @@ -275,26 +275,48 @@ def _try_nospaces_parser(self): return self._try_parser(parse_method=_parse_nospaces) def _try_parser(self, parse_method): - _order = self._settings.DATE_ORDER + original_order = self._settings.DATE_ORDER + + # Use locale date order unless DATE_ORDER was explicitly set by the caller. + if ( + self._settings.PREFER_LOCALE_DATE_ORDER + and "DATE_ORDER" not in self._settings._mod_settings + ): + first_order = self.locale.info.get("date_order", original_order) + else: + first_order = original_order + + candidates = [first_order] + + # If the caller requires a year (and not a day) and did not set DATE_ORDER, + # retry once or twice with year-biased orders to resolve month-number ambiguity. + require_parts = set(getattr(self._settings, "REQUIRE_PARTS", None) or []) + if ( + "DATE_ORDER" not in self._settings._mod_settings + and "year" in require_parts + and "day" not in require_parts + ): + for order in ("MYD", "YMD"): + if order not in candidates: + candidates.append(order) + + translated = self._get_translated_date() + try: - if self._settings.PREFER_LOCALE_DATE_ORDER: - if "DATE_ORDER" not in self._settings._mod_settings: - self._settings.DATE_ORDER = self.locale.info.get( - "date_order", _order + for order in candidates: + self._settings.DATE_ORDER = order + try: + date_obj, period = date_parser.parse( + translated, + parse_method=parse_method, + settings=self._settings, ) - date_obj, period = date_parser.parse( - self._get_translated_date(), - parse_method=parse_method, - settings=self._settings, - ) - self._settings.DATE_ORDER = _order - return DateData( - date_obj=date_obj, - period=period, - ) - except ValueError: - self._settings.DATE_ORDER = _order + return DateData(date_obj=date_obj, period=period) + except ValueError: + continue return None + finally: + self._settings.DATE_ORDER = original_order def _try_given_formats(self): if not self.date_formats: diff --git a/tests/test_clean_api.py b/tests/test_clean_api.py index 4ed18a7b3..5f75f81d9 100644 --- a/tests/test_clean_api.py +++ b/tests/test_clean_api.py @@ -185,6 +185,53 @@ def test_dates_which_do_not_match_locales_are_not_parsed( self.when_date_is_parsed(date_string, locales=locales) self.then_date_was_not_parsed() + @parameterized.expand( + [ + param(date_string="Oct-23", expected_date=datetime(2023, 10, 1, 0, 0)), + param(date_string="May-23", expected_date=datetime(2023, 5, 1, 0, 0)), + ] + ) + def test_require_parts_month_year_parses_month_year( + self, date_string, expected_date + ): + # Regression: when year is required, Mon-YY should be interpreted as month-year. + base = datetime(2050, 1, 1, 0, 0) + self.when_date_is_parsed_with_settings( + date_string, + settings={ + "RELATIVE_BASE": base, + "PREFER_DAY_OF_MONTH": "first", + "PREFER_DATES_FROM": "past", + "REQUIRE_PARTS": ["month", "year"], + }, + ) + self.then_parsed_date_and_time_is(expected_date) + + def test_require_parts_does_not_override_explicit_date_order(self): + # Explicit DATE_ORDER must be respected. + base = datetime(2050, 1, 1, 0, 0) + self.when_date_is_parsed_with_settings( + "Oct-23", + settings={ + "RELATIVE_BASE": base, + "REQUIRE_PARTS": ["month", "year"], + "DATE_ORDER": "MDY", + }, + ) + self.then_date_was_not_parsed() + + def test_require_parts_month_day_parses_month_day(self): + # If day is required, Mon-XX should remain month-day. + base = datetime(2000, 1, 1, 0, 0) + self.when_date_is_parsed_with_settings( + "Oct-23", + settings={ + "RELATIVE_BASE": base, + "REQUIRE_PARTS": ["month", "day"], + }, + ) + self.then_parsed_date_and_time_is(datetime(2000, 10, 23, 0, 0)) + def when_date_is_parsed_with_defaults(self, date_string): self.result = dateparser.parse(date_string)