日本語 · English

Nearest weekday to the 15th — Quartz 15W by composition

Translated from the canonical Japanese page recipes/quartz-15w-nearest-weekday.md.

“The 15th of every month, or the nearest weekday if it falls on a weekend” is the requirement known as Quartz’s 15W. RRULE has no such vocabulary, and even the Quartz dialect cannot see holidays. Kairos has no dedicated Nearest roll convention — instead the requirement is a finite case split written as composition, which is exactly why it extends to holidays.

What happens elsewhere

Writing it in Kairos

Split the stream of 15ths three ways — weekday as is, Saturday backward, Sunday forward — and take the union:

# eval: 2026-01-01..2026-12-31
@JP
weekdays = everyDay |> filter(d => weekday(d) != Sat and weekday(d) != Sun)
d15 = everyDay |> within(month) |> nth(15)
(d15 |> filter(d => weekday(d) != Sat and weekday(d) != Sun))
  | (d15 |> filter(d => weekday(d) == Sat) |> roll(Preceding, on: weekdays))
  | (d15 |> filter(d => weekday(d) == Sun) |> roll(Following, on: weekdays))
#=> 2026-01-15 2026-02-16 2026-03-16 2026-04-15 2026-05-15 2026-06-15
#=> 2026-07-15 2026-08-14 2026-09-15 2026-10-15 2026-11-16 2026-12-15

February, March, and November (15th on a Sunday) move to the following Monday; August (15th on a Saturday) moves back to Friday 8/14. The case split hidden inside the single word “nearest” is visible as three lines of expression.

The holiday support that 15W cannot offer is just swapping weekdays for a business-day stream:

(d15 |> filter(…)) | (… |> roll(Preceding, on: bizDay)) | (… |> roll(Following, on: bizDay))

The roll targets become business days, and what to do when the 15th itself is a holiday is one more condition on d15 |> filter — a dedicated token folds the spec shut, while composition opens for repair.

Try it in your browser

Run it in the Playground — change nth(15) for any other day of the month.