ProvenPin Install

date-fns 4 + @date-fns/tz time zone math (DST-safe): working versions (tested 2026-10-15)

Schedule and format across time zones with date-fns v4 and TZDate from @date-fns/tz: local-midnight boundaries, DST transitions and UTC storage.

passed4 passed · 0 failed2026-10-15 13:27 UTC
Status
passed
Last passed
2026-10-15 13:27 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
date-fns@4.4.0@date-fns/tz@1.5.0tsx@4.23.15@types/node@24.19.2
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38742087302

The code

Exactly the files that passed. Your agent can pull them with get_recipe("date-fns-tz-scheduling") (install), or see them on GitHub.

package.json
{
  "name": "date-fns-tz-scheduling",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "date-fns": "4.4.0",
    "@date-fns/tz": "1.5.0"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "24.19.2"
  }
}
src/schedule.ts
import { addDays, differenceInHours, format, startOfDay } from "date-fns";
import { TZDate, tz } from "@date-fns/tz";

// "9am every day in the user's zone", stored as UTC ISO strings
export function dailyAt(zone: string, startIso: string, hour: number, days: number) {
  const start = startOfDay(new TZDate(startIso, zone));
  return Array.from({ length: days }, (_, i) => {
    const d = addDays(start, i);
    d.setHours(hour, 0, 0, 0); // wall-clock hour in `zone`, even across DST
    return new Date(d.getTime()).toISOString();
  });
}

export const formatIn = (iso: string, zone: string, pattern = "yyyy-MM-dd HH:mm") => format(new TZDate(iso, zone), pattern);

// start of "today" for a user, as a UTC instant (for DB range queries)
export const startOfDayIn = (iso: string, zone: string) => new Date(startOfDay(iso, { in: tz(zone) }).getTime()).toISOString();

export const hoursBetweenMidnights = (zone: string, dayIso: string) => {
  const a = startOfDay(new TZDate(dayIso, zone));
  return differenceInHours(addDays(a, 1), a);
};
test/schedule.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { dailyAt, formatIn, hoursBetweenMidnights, startOfDayIn } from "../src/schedule.ts";

test("9am New York stays 9am across the November DST change", () => {
  assert.deepEqual(dailyAt("America/New_York", "2026-10-31T12:00:00Z", 9, 3), [
    "2026-10-31T13:00:00.000Z", // EDT (UTC-4)
    "2026-11-01T14:00:00.000Z", // EST (UTC-5)
    "2026-11-02T14:00:00.000Z",
  ]);
});

test("format in a zone", () => {
  assert.equal(formatIn("2026-07-01T12:00:00Z", "Asia/Tokyo"), "2026-07-01 21:00");
});

test("start of day for a user zone as UTC", () => {
  assert.equal(startOfDayIn("2026-10-15T05:00:00Z", "America/Anchorage"), "2026-10-14T08:00:00.000Z");
});

test("DST days are 23 or 25 hours long", () => {
  assert.equal(hoursBetweenMidnights("America/New_York", "2026-03-08T12:00:00Z"), 23);
  assert.equal(hoursBetweenMidnights("America/New_York", "2026-11-01T12:00:00Z"), 25);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.