Repository navigation
Reject a %j day of year that the parsed year does not have (#271) - #1370
Merged
Merged
Conversation
- 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>
Codecov Reportβ
All modified and coverable lines are covered by tests. 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. π New features to boost your workflow:
|
AdrianAtZyte
approved these changes
Aug 27, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #271.
Problem
datetime.strptime()resolves a%j(day of year) value past the end of the parsedyear into the next year, so day 366 of a non-leap year silently became January 1 of
the following year:
The returned year contradicts the year literally present in the input, and rejection of
invalid values was inconsistent: day 367 returned
Nonewhile 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 issuestays covered.
Fix
The std lib keeps the value it read for
%jinstruct_time.tm_ydayeven after rollingthe 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()raisesValueError, whichparse_with_formats()already handles as "this format does not match", soparse()returns
None(or falls through to the next format)._prepare_format()now also reports whether the format holds a real%jdirective,reusing the directive scan it already performs, so an escaped
%%jliteral is notmistaken for one.
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β%jvalues that are valid (month edges, both leap-yearrules, day 1/59/60/365/366), values the year does not have, values outside the
%jrange, and
%%jescaped literals.tests/test_date.pyβ the same throughparse_with_formats(), checkingperiodstays"day".tests/test_date_parser.pyβ end-to-end throughparse(), including the exact examplesfrom Parsing with %j formatΒ #271.
10 of the new parameters fail on
masterand pass with this change; the rest areregression coverage for behavior established by #1345.
Verification run locally: full suite
24231 passed, 18 skipped, 1 xfailed;ruffandruff-formatclean; an exhaustive sweep of years 1583β2400 Γ day-of-year 0β400 (328,419cases) against
1 <= j <= 365/366found zero mismatches, and thetm_ydaybehavior thecheck relies on was confirmed identical on CPython 3.10, 3.11, 3.12 and 3.14.
Out of scope
A format that contains
%jbut no year directive (e.g.date_formats=["%j"]) resolvesthe 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 dayfrom day 60 onward. That is a separate defect on the yearless-format path β it cannot be
fixed in
_prepare_format(), becausedate.pyre-stamps the year fromRELATIVE_BASEafterwards β and is left for its own change. This PR does change one case there:
parse("366", date_formats=["%j"])now returnsNoneinstead of January 1 of thecurrent year.