npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

temporals

v0.0.2

Published

Lazy sequences, ranges, intervals, and RRULE recurrence built on the TC39 Temporal API.

Readme

temporals

Lazy sequences, ranges, intervals, and RRULE recurrence built on the TC39 Temporal API.

Temporal ships the atoms of date/time — immutable points, durations, and calendar-aware arithmetic — but no sequence layer. temporals fills that gap: feed in parameters, get back a lazy collection of Temporal objects, either points (PlainDate, ZonedDateTime, …) or intervals ({ start, end } spans).

import { range } from "temporals";

// The next 10 weekdays, lazily.
range({ start: Temporal.Now.plainDateISO(), step: { days: 1 } })
  .filter((d) => d.dayOfWeek <= 5)
  .take(10)
  .toArray();

Design

  • Iterator-first. Generators return a re-iterable Seq with the standard iterator-helper surface (map/filter/take/drop/flatMap/toArray/…), so composition is native. On runtimes with native iterator helpers (Node 22+, modern browsers) those work too; Seq provides them portably so behaviour is identical on older runtimes.
  • Thin builder. seq() / recurBuilder() wrap the core generators for discoverability. They hold no logic — every terminal delegates to a generator.
  • Temporal is a peer. The engines derive everything from the values you pass in (their constructor, static compare, instance .add), so nothing is bundled. On Node < 22, install temporal-polyfill and import "temporal-polyfill/global" once at your entry point.
  • Half-open by default. Ranges and intervals are [start, end) unless you opt into inclusive bounds.
  • Calendar-correct. Stepping in calendar units keeps DST and month lengths right; month steps are anchor-relative so they don't drift (Jan 31 → Feb 28 → Mar 31, not → Mar 28).

Install

npm install temporals
# On Node < 22, also:
npm install temporal-polyfill

Documentation

  • API reference — a complete, generated reference for every export (all subpaths) lives at https://johnhenry.github.io/temporals/ (built from JSDoc via TypeDoc; run npm run docs to generate it locally into docs/).
  • Examples — runnable, per-feature programs in examples/ (range, recur, cron, intervals, business, humanize, backoff, ics, plus a reference scheduler). npm run examples runs them all.
  • This README is the guided tour below.

API

range(options): Seq<T>

A stepped sequence of points from start.

| option | meaning | | ----------- | ------------------------------------------------------------- | | start | first point (always included) | | step | increment (Temporal.Duration or { days: 1 }); negative descends | | end? | bound; upper for positive steps, lower for negative | | count? | max number of points | | inclusive?| include a point landing exactly on end (default false) | | overflow? | "constrain" (default) or "reject" for calendar arithmetic |

With neither end nor count, the sequence is infinite — bound it with .take(n).

chunks(options): Seq<Interval<T>> — partition

Adjacent, non-overlapping intervals of width by covering [start, end). The final partial chunk is clamped to end (pass partial: false to drop it).

windows(options): Seq<Interval<T>> — sliding

Overlapping windows of width size, advancing each start by step. Only full-width windows by default (partial: true emits clamped trailing windows).

recur(rule): Seq<T> — RRULE recurrence

A pragmatic subset of RFC 5545:

recur({
  start: Temporal.PlainDate.from("2026-01-01"),
  freq: "monthly",
  byWeekday: [{ weekday: "TU", nth: 2 }], // 2nd Tuesday
  count: 12,
}).toArray();

Supported: freq (yearly/monthly/weekly/daily/hourly/minutely/secondly), interval, count, until, byMonth, byWeekNo (yearly), byYearDay, byMonthDay (incl. negatives), byWeekday (incl. nth), byHour/byMinute/bySecond, bySetPos, weekStart.

Sub-daily frequencies require a time-bearing start (PlainDateTime, ZonedDateTime, or PlainTime) and treat by* rules as filters; bySetPos is not combined with sub-daily frequencies.

Also supported:

  • include (RFC 5545 RDATE) and exclude (EXDATE) — merge extra dates or drop occurrences (count counts what's actually returned).
  • DST policy for ZonedDateTime starts — dstGap: "fire" | "skip", dstOverlap: "first" | "second" (same knobs as cron).
  • Month/year stepping is calendar-aware (correct across Hebrew leap years, Islamic, Persian, … — see Scope & limitations).
  • Impossible rules throw rather than silently stopping.
  • splitSeries(rule, at){ before, after } for "this and following" edits — divides count, partitions EXDATE/RDATE, so the two halves reproduce the original. Edit after to change this-and-following instances.

recur.fromString(...), ruleFromString(...), and formatRule(rule) provide RRULE-string interop; the temporals/ics subpath adds full .ics import/export.

Interval<T>

The interval value type Temporal lacks:

const q1 = Interval.from("2026-01-01/2026-04-01");
q1.contains(someDate);
q1.overlaps(other);
q1.intersection(other);   // Interval | null
q1.toDuration();          // Temporal.Duration
q1.points({ days: 1 });   // Seq<T> of points inside
q1.toString();            // "2026-01-01/2026-04-01"

Fluent builders

seq(start).step({ days: 1 }).until(end).toArray();
seq(start).step({ days: 1 }).until(end).chunks({ weeks: 1 });

recurBuilder(start).monthly().on({ weekday: "FR", nth: -1 }).count(3).toArray();
recurBuilder(start).weekly().every(2).on("MO", "WE").toString(); // -> RRULE string

temporals/cron — Temporal-native cron

Cron lives in a subpath (temporals/cron) so the core stays lean. It's a matching schedule ("fire when the wall clock matches"), evaluated in an explicit time zone with explicit, correct DST behaviour — the main advantage over Date-based cron libraries.

import { cron, cronSchedule, describeCron } from "temporals/cron";

cron("0 9 * * 1-5", { timeZone: "America/New_York" }).take(3).toArray(); // next 3 weekday 9am fire times

Faithful to cron semantics (and different from RRULE): clock-aligned rather than anchored to a start (*/15:00 :15 :30 :45), and the day-of-month / day-of-week OR quirk (when both are restricted, either matches). Full field syntax — *, ?, ranges, steps, lists, names (JAN, MON, …); @daily/ @hourly macros; an optional leading seconds field; and Quartz day specials: L / L-n / LW / nW (day-of-month) and dL / d#n (day-of-week).

DST policy is explicit: dstGap: "fire" (default, shift forward) or "skip"; dstOverlap: "first" (default) or "second". Also parseCron, describeCron (humaniser), and best-effort cronToRule / ruleToCron converters (lossy — they return null rather than guess; L/# map to RRULE nth/last).

Schedule — one interface for cron, RRULE, and ranges

A Schedule is the unifying answer to "when does this happen?" — pure, never executes. Cron, recur, and range all compile to it.

import { Schedule } from "temporals";
import { cronSchedule } from "temporals/cron";

const s = cronSchedule("0 9 * * 1-5", { timeZone: "America/New_York" });
s.next(now);              // next fire strictly after `now`
s.nextN(now, 5);          // next 5
s.between(start, end);    // occurrences in [start, end)

Schedule.rule({ start, freq: "monthly", byWeekday: [{ weekday: "TU", nth: 2 }] });
Schedule.range({ start, step: { days: 1 } });

See examples/scheduler for a minimal reference scheduler that runs a Schedule (the when vs do it boundary).

Calendar rounding & bucketing

import { startOf, endOf, quarterOf, fiscalQuarterOf } from "temporals";

startOf(dt, "week", { weekStart: "MO" });   // floor to a unit
endOf(dt, "month");                          // exclusive upper bound (start of next)
quarterOf(date);                             // 1–4
fiscalQuarterOf(date, 10);                   // fiscal year starting in October

Units: year quarter month week day hour minute second. endOf is the exclusive next-unit start, so [startOf(p,u), endOf(p,u)) is the bucket.

Interval algebra & sets

Interval gains Allen's 13 relations (a.relation(b)meets/overlaps/ during/…). IntervalSet is a normalized (merged, sorted) set with the operations you need for free/busy availability:

import { Interval, IntervalSet } from "temporals";

const free = work.difference(busy);   // union / intersection / difference / gaps
free.totalDuration();                 // summed coverage

conflicts(bookings);                  // [ [a, b], … ] overlapping pairs (detection; policy is yours)

See examples/availability.mjs — working hours − meetings = open slots.

temporals/business — working time

import {
  BusinessCalendar, Holidays, usFederalHolidays, easterHoliday,
  WorkingHours, businessDuration, meetingSlots,
} from "temporals/business";

const cal = new BusinessCalendar({ holidays: usFederalHolidays() }); // or build your own rules
cal.isBusinessDay(date);
cal.addBusinessDays(date, 5);
cal.nthBusinessDay(2026, 1, -1);       // last business day of the month (payroll)

const hours = new WorkingHours({ windows: [["22:00", "06:00"]], calendar: cal }); // overnight OK
businessDuration(start, end, hours);   // elapsed working time (skips weekends/holidays/off-hours)

// Mutual availability across people, each in their own zone:
const slots = meetingSlots({
  participants: [{ hours, timeZone: "America/New_York" }, { hours, timeZone: "America/Los_Angeles" }],
  within, duration: { minutes: 30 },
});
// Each slot carries per-participant local times, so ranking is a one-liner:
slots.sort((a, b) => a.latestLocalHour - b.latestLocalHour); // "not too late for anyone"

Holiday rules reuse the same nth-weekday logic as RRULE (Thanksgiving is nthWeekdayHoliday(11, "TH", 4)); easterHoliday(-2) is Good Friday. observed supports "us" / "uk" weekend-shift styles. meetingSlots is the availability substrate — it hands you localStarts / earliestLocalHour / latestLocalHour per slot so ranking is trivial, but the ranking policy is yours.

temporals/humanize — durations & relative time

import { humanizeDuration, formatRelative, fromNow, parseDuration } from "temporals/humanize";

humanizeDuration(Temporal.Duration.from({ hours: 2, minutes: 3 })); // "2 hours, 3 minutes"
humanizeDuration(dur, { locale: "fr" });          // localized via Intl.DurationFormat when available
parseDuration("1h30m");                          // Temporal.Duration
formatRelative(from, to);                         // "in 5 days" (via Intl.RelativeTimeFormat)
fromNow(someDate);                                // "3 days ago"

temporals/ics — iCalendar import/export

import { toICS, fromICS, icsToSeq } from "temporals/ics";

const ics = toICS([{ start, rrule: "FREQ=WEEKLY;COUNT=4", exdate: [skipDay] }]);
const [event] = fromICS(ics);       // Temporal values + RRULE string
icsToSeq(event).toArray();          // expand DTSTART + RRULE + EXDATE/RDATE

Backoff & DST helpers

import { backoff, isDST, nextTransition, transitionsBetween } from "temporals";

backoff({ base: 100, max: 5000, jitter: "equal", attempts: 6 }); // Seq<Duration> of retry delays
isDST(zdt);                    // heuristic (offset vs standard)
nextTransition(zdt);           // next UTC-offset change, or null
transitionsBetween(a, b);      // all transitions in a range

Natural-language date parsing ("next Tuesday") is intentionally not in core — it lives in a separate extension that wraps temporals.

Supported point types

PlainDate, PlainDateTime, ZonedDateTime (DST-correct), PlainYearMonth, PlainTime, and Instant for range/chunks/windows. Recurrence (recur) requires a date-bearing start (PlainDate/PlainDateTime/ZonedDateTime). cron always produces ZonedDateTime.

Scope & limitations

Being upfront about the edges:

  • Proleptic Gregorian. Like Temporal, dates use the proleptic Gregorian calendar — there is no historical Julian→Gregorian 1582 cut-over (no skipped days, no per-country adoption). Not suitable for historical dating before ~1582.
  • Non-ISO calendars (partial). range, intervals, startOf (day/week/month/ year), business-day/weekday logic, and recur month/year stepping are calendar-aware and work with Hebrew, Islamic, Persian, etc. (needs temporal-polyfill/full or native Temporal + ICU). Not generalized: quarterOf/fiscalQuarterOf assume 12-month years; byWeekNo/byYearDay are ISO-oriented; the Chinese calendar's leap months (numeric month vs monthCode) are only best-effort. cron is Gregorian civil time by design.
  • isDST is a definition, not a law. Defined as "offset above the year's minimum offset." Correct for standard summer-DST zones; negative-DST zones (e.g. Europe/Dublin) are inherently ambiguous — treat as advisory.
  • Leap seconds are not modeled (Temporal doesn't — it uses a POSIX-like time scale). Durations won't reflect them.
  • meetingSlots finds availability, not the optimal time. It returns the windows when everyone is free; scoring by preference, timezone fairness, or fragmentation is a solver concern left to the caller.
  • Schedule isn't serializable as such (it's an opaque occurrence function); serialize the source instead — cron string, formatRule(rule), or .ics.
  • The reference scheduler (examples/scheduler) is a demo, not a durable/clustered job runner.

License

MIT