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/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 `__! 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/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/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/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/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_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) 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"), 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"), 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