Find the Next Occurrence of a Weekday in JavaScript
To find the next given weekday, add ((target - current + 7) % 7) days to a Temporal.PlainDate, choosing whether a match today counts. Part of Date Arithmetic Without Mutations.
Why this scenario is tricky
"Find the next Tuesday" sounds trivial until you pin down what "next" means, and that ambiguity is the whole difficulty. If today is already Tuesday, does "next Tuesday" mean today or seven days from now? If today is Wednesday, the answer is six days out; if today is Monday, it is one day out. Every one of those cases is a different offset, and a naive implementation that loops day by day checking dayOfWeek is both slow and easy to get wrong at the same-day boundary. The clean solution replaces the loop with a single piece of modular arithmetic, but that arithmetic only works if you are precise about the day-of-week numbering.
Temporal uses the ISO 8601 convention for dayOfWeek: Monday is 1 and Sunday is 7. This differs from legacy JavaScript, where Date.prototype.getDay() returns 0 for Sunday through 6 for Saturday — a different origin and a different starting day of the week. Porting a weekday calculation from getDay() to dayOfWeek without adjusting for both differences produces answers that are wrong by a variable amount depending on the target day, which is exactly the kind of bug that passes a quick test on Monday and fails silently on the weekend. Getting the numbering straight is the prerequisite for the formula that follows.
The second subtlety is that this is arithmetic on a civil date, not on absolute time. Because PlainDate has no clock and no zone, adding a number of days always moves by whole calendar days, so the result is immune to daylight-saving transitions. Try the same "add N days" logic on a millisecond timestamp and a spring-forward night can leave you an hour short of the day boundary, occasionally landing you on the wrong date. Doing the work on a zoneless type is what makes the answer correct everywhere without special cases.
The whole problem is the modular step and the today-counts decision. With dayOfWeek running 1 (Monday) to 7 (Sunday), the days to add is (target - today + 7) % 7; if that is 0 you are already on the target, and whether to return today or jump a full week is a product decision you must make explicit.
Minimal working solution
The expression ((target - from.dayOfWeek + 7) % 7) || 7 reads as: compute the raw forward distance from today's weekday to the target weekday, normalize it into the range 0–6 by adding 7 and taking the remainder, and then treat a zero result as a full week. The + 7 before the modulo is what handles the wrap-around when the target is earlier in the week than today — without it, the subtraction could go negative and the remainder of a negative number is not the value you want. The trailing || 7 encodes the default decision that "next Tuesday" from a Tuesday means seven days out rather than today, because the raw distance in that case is 0 and 0 || 7 evaluates to 7.
Once the offset is computed, from.add({ days: add }) produces the answer as a new immutable PlainDate, leaving the input untouched. That immutability is quietly important: a scheduling routine often computes several candidate dates from the same anchor, and if the "add days" step mutated the anchor the way legacy Date.setDate() does, each subsequent calculation would start from the wrong place. With Temporal every add returns a fresh value, so the anchor stays put and the code reads as a series of pure transformations.
import { Temporal } from '@js-temporal/polyfill';
function nextWeekday(from: Temporal.PlainDate, target: number /*1..7*/) {
const add = ((target - from.dayOfWeek + 7) % 7) || 7; // never today; next week if same
return from.add({ days: add });
}
Full production version
The production version makes the same-day policy an explicit parameter rather than a buried default, because real applications genuinely disagree about it. A "remind me next Friday" feature almost always means the following Friday even if today is Friday, so it wants includeToday = false. A "next available Monday, starting from this date" scheduler that treats the anchor as inclusive wants includeToday = true, so that if the anchor is already a Monday it is accepted as-is. Encoding that choice in the signature means the caller states intent at the call site and a reader never has to guess which convention a given use adopts.
From this one primitive you can build the higher-level scheduling operations most calendars need. "The second Tuesday of next month" is a matter of jumping to the first day of the target month and then finding the first Tuesday on or after it, then adding a week. "Every other Thursday starting from a date" is repeated application of nextWeekday with a fortnight step. "The last Friday of the quarter" runs the same logic backwards from the quarter's end. Because each of these composes cleanly on top of a correct, immutable next-weekday function, the hard part — the modular arithmetic and the same-day policy — is solved once and reused, rather than re-derived (and re-broken) in each feature.
It is also worth keeping the weekday represented as the ISO number rather than a localized string inside the logic, and translating to a display name only at the edge. Weekday names are locale-dependent and their order shifts — some locales treat Sunday as the first day of the week for presentation — but the underlying arithmetic should always run on the stable ISO 1–7 numbering. Separating the calculation from the presentation keeps the scheduler correct for users everywhere while still letting the UI say "next Tuesday" in the user's own language.
import { Temporal } from '@js-temporal/polyfill';
function nextWeekday(from: Temporal.PlainDate, target: number, includeToday = false) {
const raw = (target - from.dayOfWeek + 7) % 7;
const add = raw === 0 ? (includeToday ? 0 : 7) : raw;
return from.add({ days: add });
}
Verification snippet
The cases that expose bugs are the boundaries, so test all seven relative positions plus the same-day policy. From a known Wednesday, assert that the next Wednesday is seven days out when includeToday is false and zero days out (today) when it is true — this is the single case most implementations get wrong. Assert that the next Thursday from that Wednesday is one day out and that the next Tuesday is six days out, which proves the wrap-around handling in both the short-forward and long-forward directions.
Add a week-boundary case that crosses into the next calendar week and a month-boundary case that crosses into the next month, to confirm that PlainDate.add rolls over correctly rather than clamping. Finally, run the whole suite under several TZ environment values and assert the results never change; because the computation is on a zoneless PlainDate, a host-zone dependence would indicate that an instant leaked into the logic somewhere. A green result across zones is the proof that the arithmetic is genuinely calendar-based.
Common pitfalls
The most frequent pitfall is mismatched weekday numbering: mixing Temporal's Monday-is-1 dayOfWeek with legacy Sunday-is-0 getDay() values, or hard-coding a target like "3 for Wednesday" using the wrong convention. Pick one numbering — the ISO 1–7 that Temporal uses — and define named constants for the target days so a 3 never floats through the code ambiguously. The second pitfall is forgetting the same-day case entirely, which yields a function that either always skips a matching anchor or always includes it, neither of which is right for every caller; making it a parameter removes the guesswork.
A third mistake is reaching for a day-by-day loop instead of the modular formula. The loop works but is O(7) in the worst case, allocates up to seven intermediate dates, and tends to hide its own same-day bug inside the loop condition. The closed-form offset is a single subtraction, a modulo, and one add, which is both faster and easier to reason about. A related trap is computing the offset correctly but then adding it to the wrong anchor — for instance, to Temporal.Now.plainDateISO() read separately rather than to the from value passed in — which reintroduces a dependence on the current moment that a test cannot control. Always add to the explicit anchor the caller supplied.
Frequently Asked Questions
How do I find the next Monday from a given date?
Use dayOfWeek (1 = Monday … 7 = Sunday) and add ((target - current + 7) % 7) days to a Temporal.PlainDate. Adding 7 before the modulo keeps the result in the future; if the remainder is 0 you are on the target already.
Should the current day count as the next occurrence?
That is a product decision, so make it explicit with a flag. When the modular difference is 0, either return today (includeToday) or add a full week to get the following occurrence.
How do I decide whether 'next Tuesday' includes today if today is Tuesday?
Make it an explicit parameter rather than a silent default. Compute the raw offset (target - dayOfWeek + 7) % 7; when it is zero the anchor already falls on the target weekday, so return the anchor for an inclusive policy or add seven days for an exclusive one. Different features genuinely want different answers — a reminder usually means the following Tuesday, a scheduler often accepts today — so exposing the choice at the call site avoids a wrong assumption.
Why does Temporal number weekdays differently from legacy Date?
Temporal follows ISO 8601, where dayOfWeek is 1 for Monday through 7 for Sunday. Legacy Date.getDay() returns 0 for Sunday through 6 for Saturday — a different origin and a different first day. Porting a weekday calculation between them requires adjusting for both differences, otherwise the result is wrong by a variable amount depending on the target day. Standardize on the ISO 1–7 numbering inside your logic and convert to display names only at the UI edge.
Is finding the next weekday safe across daylight-saving changes?
Yes, when you compute it on a Temporal.PlainDate. Because PlainDate has no clock or zone, adding a number of days always moves by whole calendar days, so a range that crosses a spring-forward or fall-back night still lands on the correct date. The same 'add N days' logic applied to a millisecond timestamp can drift by an hour across a DST transition and occasionally land on the wrong day, which is why the calendar type is the right tool here.