Skip to content

Reject a %j day of year that the parsed year does not have (#271) - #1370

Merged
serhii73 merged 13 commits into
masterfrom
fix-271-day-of-year
Aug 28, 2026
Merged

serhii73 merged 13 commits into
masterfrom
fix-271-day-of-year

Conversation

@serhii73

Copy link
Copy Markdown
Collaborator

Closes #271.

Problem

datetime.strptime() resolves a %j (day of year) value past the end of the parsed
year into the next year, so day 366 of a non-leap year silently became January 1 of
the following year:

>>> dateparser.parse("1999366", date_formats=["%Y%j"])
datetime.datetime(2000, 1, 1, 0, 0)   # 1999 has 365 days
>>> dateparser.parse("2023-366", date_formats=["%Y-%j"])
datetime.datetime(2024, 1, 1, 0, 0)

The returned year contradicts the year literally present in the input, and rejection of
invalid values was inconsistent: day 367 returned None while day 366 returned a date.

This is the part of #271 left over after #1345. The other two cases from the original
report ("1999001" β†’ Jan 1, "1999050" β†’ Feb 19) were fixed by #1345, and "1999777"
already returned None; regression tests for all three are included here so the issue
stays covered.

Fix

The std lib keeps the value it read for %j in struct_time.tm_yday even after rolling
the date into the next year, so the rollover is detected by comparing it with the day of
year of the resulting date. When the two differ, strptime() raises ValueError, which
parse_with_formats() already handles as "this format does not match", so parse()
returns None (or falls through to the next format).

_prepare_format() now also reports whether the format holds a real %j directive,
reusing the directive scan it already performs, so an escaped %%j literal is not
mistaken for one.

>>> dateparser.parse("1999366", date_formats=["%Y%j"])       # None
>>> dateparser.parse("2000366", date_formats=["%Y%j"])       # 2000-12-31 (leap year)
>>> dateparser.parse("1999365", date_formats=["%Y%j"])       # 1999-12-31

Week-based directives that legitimately roll the year over (%U/%W/%w, %G/%V/%u)
are untouched β€” the check only applies to formats that contain a real %j.

Tests

  • tests/test_utils_strptime.py β€” %j values that are valid (month edges, both leap-year
    rules, day 1/59/60/365/366), values the year does not have, values outside the %j
    range, and %%j escaped literals.
  • tests/test_date.py β€” the same through parse_with_formats(), checking period stays
    "day".
  • tests/test_date_parser.py β€” end-to-end through parse(), including the exact examples
    from Parsing with %j formatΒ #271.

10 of the new parameters fail on master and pass with this change; the rest are
regression coverage for behavior established by #1345.

Verification run locally: full suite 24231 passed, 18 skipped, 1 xfailed; ruff and
ruff-format clean; an exhaustive sweep of years 1583–2400 Γ— day-of-year 0–400 (328,419
cases) against 1 <= j <= 365/366 found zero mismatches, and the tm_yday behavior the
check relies on was confirmed identical on CPython 3.10, 3.11, 3.12 and 3.14.

Out of scope

A format that contains %j but no year directive (e.g. date_formats=["%j"]) resolves
the day of year against the std lib's default year 1900 and only afterwards gets a year
stamped on it in parse_with_formats(), so in a leap effective year it is off by one day
from day 60 onward. That is a separate defect on the yearless-format path β€” it cannot be
fixed in _prepare_format(), because date.py re-stamps the year from RELATIVE_BASE
afterwards β€” and is left for its own change. This PR does change one case there:
parse("366", date_formats=["%j"]) now returns None instead of January 1 of the
current year.

serhii73 and others added 13 commits June 22, 2026 11:16
- Add month locative/dative forms (lednu, ΓΊnoru, bΕ™eznu, dubnu, kvΔ›tnu,
  červnu, červenci, srpnu, Ε™Γ­jnu, prosinci) for dates like "v lednu 2023"
- Add July abbreviation "črv"
- Fix "za mΔ›sΓ­c/tΓ½den/rok" not parsing: add accusative simplification
  patterns (previous patterns only matched instrumental -em forms)
- Add tests covering the new expressions

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
datetime.strptime resolves a day of year past the end of the parsed year
into the next year, so day 366 of a non-leap year became January 1 of the
following year:

    >>> dateparser.parse("1999366", date_formats=["%Y%j"])
    datetime.datetime(2000, 1, 1, 0, 0)

The returned year contradicted the year in the input, and invalid values
were rejected inconsistently: day 367 gave None while day 366 gave a date.

The std lib keeps the value it read for %j in struct_time.tm_yday even
after rolling the date into the next year, so the rollover can be spotted
by comparing it with the day of year of the resulting date. When the two
differ, strptime() now raises ValueError, which parse_with_formats()
already handles as "this format does not match".

_prepare_format() now also reports whether the format holds a real %j
directive, so that an escaped "%%j" literal is not mistaken for one.

Fixes #271.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codspeed

codspeed Bot commented Aug 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

βœ… 7 untouched benchmarks


Comparing fix-271-day-of-year (2ca05ed) with master (cd9bb13)

Open in CodSpeed

@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 97.18%. Comparing base (cd9bb13) to head (2ca05ed).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1370   +/-   ##
=======================================
  Coverage   97.18%   97.18%           
=======================================
  Files         236      236           
  Lines        3053     3059    +6     
=======================================
+ Hits         2967     2973    +6     
  Misses         86       86           

β˜” View full report in Codecov by Harness.
πŸ“’ Have feedback on the report? Share it here.

πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@serhii73
serhii73 requested a review from AdrianAtZyte August 27, 2026 10:47
@serhii73
serhii73 merged commit 6b5f17d into master Aug 28, 2026
20 checks passed
@serhii73
serhii73 deleted the fix-271-day-of-year branch August 28, 2026 11:16
@serhii73 serhii73 mentioned this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parsing with %j format

2 participants