/calendar
// import { LIFETIME, cycleKey, cycleWindow, monthKey, monthWindow } from "pricemeter/calendar";const t = Date.parse("2026-09-30T22:30:00Z"); // already October 1st in Istanbul (UTC+3)
monthKey({ at: t }); // "2026-09"monthKey({ at: t, tz: "Europe/Istanbul" }); // "2026-10"monthWindow({ at: t, tz: "Europe/Istanbul" }); // { from: 2026-09-30T21:00Z, to: 2026-10-31T21:00Z }
// A subscription started on Jan 31st renews on the last day of shorter months.const anchor = Date.parse("2026-01-31T00:00:00Z");cycleKey({ at: Date.parse("2026-03-10T00:00:00Z"), anchor }); // "2026-02-28"cycleWindow({ at: Date.parse("2026-03-10T00:00:00Z"), anchor }); // { from: 2026-02-28, to: 2026-03-31 }
LIFETIME; // "*" — the period key for counters that never resetThe core reads no calendar (A72); what “this period” means is the application’s question. pricemeter/calendar gives pure helpers to answer it: keys that pick which period a usedSoFar counter belongs to, and windows for integrate() and counter queries.
| API | Returns |
|---|---|
monthKey({ at, tz? }) |
"2026-09": the calendar month in time zone tz (default UTC) |
monthWindow({ at, tz? }) |
That month’s half-open [from, to) window, epoch ms |
cycleKey({ at, anchor, tz? }) |
"2026-09-15": the local start date of the cycle anchored on anchor’s day of month |
cycleWindow({ at, anchor, tz? }) |
That cycle’s [from, to) window. A cycle started on the 31st renews on the last day of shorter months |
LIFETIME |
"*": the period key for counters that never reset (B16) |
Time zones are resolved with Intl; days whose local midnight does not exist (DST at 00:00) are handled. Windows are half-open: a period-end observation uses at: window.to − 1, because window.to selects the next period (A98).
Inside getRate
Section titled “Inside getRate”const mem = memoryAdapters({ rates: { sms: flat(0) }, periodOf: (at) => monthKey({ at, tz: "Europe/Istanbul" }) });const metering = buildMetering() .context<{ accountId: string; tz: string }>() .refs(["send"]) .meter("sms") .getRate(async (meter, _dims, ctx, at) => ({ rate: { model: "graduated", tiers: [{ from: 0, unitPriceMicroUsd: 0 }, { from: 500, unitPriceMicroUsd: 20_000 }], policy: { tierPeriod: "month" } }, usedSoFar: mem.store.used(ctx.accountId, meter, monthKey({ at, tz: ctx.tz })), // this local month's counter })) .commit(mem.commit);The counter store and getRate must use the same key: here memoryAdapters({ periodOf }) counts rows by local month and getRate reads the same month’s counter. policy.tierPeriod: "month" records that choice on the tariff; the library does not read it. With SQLite you use a window instead of a key: usedSoFar({ account, meter, from, to }).
How you’d do it without this
Section titled “How you’d do it without this”For a UTC calendar month, new Date(at).toISOString().slice(0, 7) is enough. The module earns its place with non-UTC time zones (22:30 UTC on Sep 30 is already October in Istanbul), cycles anchored on the subscription day (Jan 31 → Feb 28 → Mar 31) and DST transitions. If you already have a date library (Temporal, date-fns-tz, Luxon), produce the same key with it; the library never knows where the key came from.