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