Skip to content

Support "last/next/this <weekday>" (fixes #573) - #1357

Closed
gaoxiaoliang wants to merge 2 commits into
scrapinghub:masterfrom
gaoxiaoliang:feature/last-next-weekday
Closed

Support "last/next/this <weekday>" (fixes #573)#1357
gaoxiaoliang wants to merge 2 commits into
scrapinghub:masterfrom
gaoxiaoliang:feature/last-next-weekday

Conversation

@gaoxiaoliang

@gaoxiaoliang gaoxiaoliang commented Jul 28, 2026

Copy link
Copy Markdown

Reworked per the design discussion in #573. Thanks to @AdrianAtZyte for the detailed review; this replaces the earlier adjacent-week approach.

What

Parse relative weekday expressions like next friday, last monday, this sunday, which previously returned None. Fixes #573; also covers #1177 and #635.

>>> import dateparser
>>> from datetime import datetime
>>> base = datetime(2015, 6, 15)  # a Monday
>>> dateparser.parse('next friday', settings={'RELATIVE_BASE': base})
datetime.datetime(2015, 6, 19, 0, 0)
>>> dateparser.parse('el próximo martes', languages=['es'], settings={'RELATIVE_BASE': base})
datetime.datetime(2015, 6, 16, 0, 0)

Semantics (nearest occurrence)

Agreed in #573. No calendar-week concept, no week-start dependency, no new setting:

  • next <weekday>: first occurrence strictly after the base date.
  • last <weekday>: most recent occurrence strictly before the base date.
  • this <weekday>: the coming occurrence, with the base's own weekday mapping to itself.

A modifier overrides PREFER_DATES_FROM for that string. On the base date's own weekday, last/next are exactly -/+7 days (so "last Wednesday"/"next Wednesday" said on a Wednesday are unambiguous).

All locales, not English-only

The weekday relative phrases already exist in CLDR but were filtered out on import. get_cldr_data.py now maps each locale's phrase (el próximo martes, nächsten Dienstag, mardi prochain, ...) to a canonical English target (next tuesday), which the parser then resolves. This reuses the existing relative-type translation mechanism.

Collisions are handled:

  • A phrase identical to a bare weekday name (nb torsdag = "Thursday" and "this Thursday") is skipped.
  • A phrase already claimed by another relative-type entry (sr prošle nedelje = "last week" and "last Sunday") is skipped so existing behavior wins.
  • this <weekday> (identical to the bare weekday in many locales) and the English past synonym are supplied via supplementary data instead of CLDR.

No search_dates false positives

A modifier is only a date component when immediately followed by a weekday, so the last three commits, this April, and last updated ... do not match. Doubled/trailing modifiers (next next friday, friday next) are rejected.

Tests & docs

  • 7 base-weekday × 7 target-weekday × {last, next, this} matrix, base-equals-target case, PREFER_DATES_FROM override, malformed-input rejection, and search_dates positive/negative cases.
  • Full suite passes (24k+) with calendars and langdetect extras; pre-commit clean.
  • Data regenerated via get_cldr_data.py + write_complete_data.py (CLDR 44.1.0).
  • README example added.

Addressed from the review

  • Nearest-occurrence default (not adjacent week); PREFER_WEEK setting removed.
  • CLDR route for all locales instead of English-first.
  • _weekday_modifier initialized in __init__; the bare-weekday branch is extracted into a helper rather than re-indented.
  • README example corrected.

@gaoxiaoliang
gaoxiaoliang force-pushed the feature/last-next-weekday branch from bd74217 to ee81dc6 Compare July 28, 2026 09:54
@AdrianAtZyte

Copy link
Copy Markdown
Contributor
This is what Opus is telling me, thoughts?
The headline

You answered their three design questions in #573 on 2026-07-28 — and the PR does the opposite on all three, without engaging with the pushback:

┌────────────────────────────────────────────────────────────────┬─────────────────────────────────────────┐
│                         Your position                          │            What the PR does             │
├────────────────────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ Q1: nearest-occurrence, not adjacent-week                      │ adjacent is the default; nearest is     │
│                                                                │ opt-in                                  │
├────────────────────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ Q2: no new setting; if one is ever needed it shouldn't be      │ New PREFER_WEEK, in the PREFER_* family │
│ PREFER_*                                                       │                                         │
├────────────────────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ Q3: extend get_cldr_data.py and get ~all locales mechanically  │ English-only, follow-up promised        │
└────────────────────────────────────────────────────────────────┴─────────────────────────────────────────┘

They never replied in the issue. That's the thing to resolve first — the rest is detail.

And your concern #3 reproduces exactly. Base Friday 2015-06-05:

this monday  -> 2015-06-01   # 4 days in the past
monday       -> 2015-06-01   # identical, so "this" adds nothing
next monday  -> 2015-06-08

this <weekday> is a no-op relative to the bare weekday under default settings and resolves a forward-looking phrase backwards. The 6 this tests all use a Monday base, which is the one base day where the problem is invisible.

Real regressions I reproduced

1. New search_dates false positives. The guard at dictionary.py:155 only rejects token strings containing nothing but modifiers. A modifier plus a bare number is enough:

"the last three commits"                    master: None   PR: [('the last three', 2015-03-01)]
"The last two items were removed"           master: None   PR: [('The last two', 2015-02-16)]

This is the exact class of false positive the PR's own guard is advertised to prevent, and it's plausible text.

2. Modifiers before months are silently absorbed into the match span. Out-of-scope is fine; widening the span is not:

"last April was warm"           master: [('April', 2015-04-16)]        PR: [('last April', 2015-04-16)]
"we met last friday, june 12"   master: [… ('friday, june 12', …)]     PR: [… ('last friday, june 12', …)]

The span now claims last was interpreted when it was discarded. A caller who strips matched spans can no longer tell. Better to leave the modifier out of the span until it's supported.

3. The README example is wrong. README.rst:152 claims parse('next friday') → 2015-06-05. The surrounding examples fix "now" at 2015-06-01 (a Monday), and the PR's own default returns 2015-06-12. 06-05 is the nearest answer — the doc shows behavior the PR doesn't implement.

What holds up

Credit where due — I verified these rather than taking the description's word:

- The 7 base weekdays × 7 target weekdays × {last, next, this} × {adjacent, nearest} matrix matches the documented spec with zero mismatches. The calendar math in _weekday_offset is right, DEFAULT_START_OF_WEEK is honored, and the result always lands on the requested weekday.
- Every trap I flagged is clear: next/last month|week|year|decade unchanged, _token_month write at parser.py:588 preserved, period='day', next friday at 5pm works, no fight with detect_time_span's next week regex.
- 24110 passed, 22 skipped, 1 xfailed. pre-commit clean. Both claims accurate.
- last/next do override PREFER_DATES_FROM — but nothing tests it, which was on my list.

Smaller code notes

- parser.py:28 — WEEKDAY_MODIFIERS maps "past": "past", which is unreachable: translate() canonicalizes past friday → last friday before the parser sees it. The remaining "last": "past" uses "past" as an internal direction name while past is also a surface form. Confusing pair.
- parser.py:546 — getattr(self, "_weekday_modifier", None) on an attribute never initialized in __init__. Initialize it to None.
- The modifier applies regardless of position and only the last one wins: thursday next → 2015-02-26, next next friday → a single next. Should probably be rejected.
- The pre-existing PREFER_DATES_FROM block is re-indented wholesale into an else:, making the diff larger than the change. Extracting it would help review.
- Test matrix is one base weekday (Monday), not the 7×7×3 I suggested; my brute-force pass says the logic is fine, but the tests wouldn't catch a regression on other base days.

My read

The engineering is careful and the diagnosis in the issue was accurate. But the semantics are the whole disagreement, and reversing your stated position on all three questions in code rather than in the thread isn't a reviewable path forward. I'd ask them to answer #573 first, and note the three concrete regressions above as blocking regardless of which semantics win.

Parse relative weekday expressions like "next friday", "last monday" and
"this sunday", which previously returned None (scrapinghub#573, scrapinghub#1177, scrapinghub#635).

Semantics use the nearest occurrence in the modifier's direction, which
needs no notion of a calendar week or its start day:

  * "next <weekday>": first occurrence strictly after the base date.
  * "last <weekday>": most recent occurrence strictly before.
  * "this <weekday>": the coming occurrence, with the base's own weekday
    mapping to itself.

A weekday modifier overrides PREFER_DATES_FROM for that string. On the
base date's own weekday, "last"/"next" are exactly -/+7 days.

The weekday relative phrases already exist in CLDR but were filtered out
on import. get_cldr_data.py now maps each locale's phrase (e.g. es "el
próximo martes", de "nächsten Dienstag") to a canonical English target
("next tuesday"), so the feature works across ~all locales rather than
English only. Phrases that collide with a bare weekday name (nb "torsdag")
or with an existing relative-type entry (sr "prošle nedelje" = "last week")
are skipped. "this <weekday>" and the "past" synonym, which CLDR does not
provide cleanly, are supplied for English via supplementary data.

A modifier that is not immediately followed by a weekday is not treated as
a date, so "the last three commits" and "last updated ..." do not yield
false positives in search_dates. Doubled or trailing modifiers ("next next
friday", "friday next") are rejected.

Tests cover a 7 base-weekday x 7 target-weekday x {last,next,this} matrix,
the base-equals-target case, PREFER_DATES_FROM override, malformed inputs,
and search_dates positive/negative cases.
@gaoxiaoliang
gaoxiaoliang force-pushed the feature/last-next-weekday branch from ee81dc6 to b4552de Compare July 28, 2026 13:23
@AdrianAtZyte

Copy link
Copy Markdown
Contributor

Can you review your own changes and let me know if you see any issues with them?

"this friday" worked but "this fri" and "this f" returned None, while the
CLDR-sourced "last"/"next" accepted their short and narrow forms. The
supplementary "this <weekday>" entries only listed the full names.

Add the short and narrow forms (mirroring CLDR's "last"/"next" output) so
"this fri"/"this f" resolve like "next fri"/"next f". These behave exactly
as the existing "next"/"last" abbreviations, including in search_dates.
@gaoxiaoliang

Copy link
Copy Markdown
Author

Went back through it properly. One thing I think needs your call before it's mergeable, one thing I already fixed, and a few notes on stuff I checked so you don't have to re-verify.

The one worth deciding on: this and the past synonym are English-only right now. Every locale gets last/next from CLDR, but CLDR's relative-type-0 ("this X") is in a lot of languages just the bare weekday name (nb torsdag, sr u nedelju), so importing it would shadow the plain weekday. I skipped it for that reason and only added this back for English via supplementary data. So a German user gets nächsten Dienstag but not diesen Dienstag. I think that's a reasonable first cut, but it's the obvious gap and I'd rather flag it than pretend it isn't there. If you want this/past for other locales in this PR I can do it — it'd mean, per language, checking whether the "this" phrase actually differs from the weekday name and only adding it where it does.

Already fixed (last commit): this fri and this f used to return None while next fri/next f worked, because I'd only added the full weekday names for this. Added the short/narrow forms to match. Confirmed they behave the same as the existing next/last abbreviations, including that this m doesn't grab anything next m wouldn't.

Things I checked that look fine:

  • The data diff is big (~21k lines, 408 files) but it's basically all additive — one deletion, a bracket reshuffle in fr. Both the CLDR JSON and the generated .py get rewritten, same as the 44.1.0 bump in Update CLDR data to 44.1.0 and add hour/duration support #1343, so it's convention rather than me bloating it.
  • 204/205 locales picked up entries. The odd one out is tl, which has no dateFields in CLDR at all, so its relative-type was already empty.
  • Round-tripped 20 unrelated locales (ru, ar, he, ja, zh, ko, th, hi, ...) — each resolves its own "next tuesday" phrase to a Tuesday, RTL and CJK included.
  • No junk short phrases got in. The two collision filters are what keep them out: skip a phrase equal to a bare weekday name, and skip one already claimed by another relative-type entry (sr prošle nedelje is both "last week" and "last Sunday", and "last week" wins). Both were found by things actually breaking, not guessed.
  • next week and next <weekday> don't collide in languages where the weekday phrase contains the word for week (nl volgende week dinsdag, sv tisdag nästa vecka).

And one non-issue for completeness: next friday 2015 returns the base date and drops the modifier, but that's just the existing weekday-plus-explicit-year behavior (friday 2015 does the same on master), so it inherits it. Left it alone.

@AdrianAtZyte

Copy link
Copy Markdown
Contributor

Closing. I prefer that this complex a change has a human in the loop.

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.

dateparser not able to parse things like next tuesday.

2 participants