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

strictdatetime

v1.3.0

Published

Strict, immutable, zero-dependency date and time utilities for JavaScript and TypeScript

Readme

strictdatetime

Strict, immutable date and time utilities for JavaScript and TypeScript with zero runtime dependencies.

Documentation · npm

npm install strictdatetime
import {
  addCalendarToZonedDateTime,
  createCalendarDuration,
  parsePlainDateTime,
  resolveZonedDateTime,
  toZonedDateTimeString,
} from "strictdatetime";

const meeting = resolveZonedDateTime(
  parsePlainDateTime("2026-03-07T12:00:00.000"),
  "America/New_York",
);
const tomorrow = addCalendarToZonedDateTime(
  meeting,
  createCalendarDuration({ years: 0, months: 0, weeks: 0, days: 1 }),
);

toZonedDateTimeString(tomorrow);
// 2026-03-08T12:00:00.000-04:00[America/New_York]

Design

  • Pure named functions and frozen structural records.
  • Separate exact elapsed-time and calendar wall-time arithmetic.
  • Strict typed parsers. Native Date.parse strings are never accepted implicitly.
  • Millisecond precision; non-zero finer precision is rejected.
  • IANA zones through the host runtime's Intl data, plus fixed-offset zones.
  • ESM and CommonJS builds with shared TypeScript declarations.
  • No runtime or peer dependencies.

Values

The public records are Instant, PlainDate, PlainTime, PlainDateTime, ZonedDateTime, ExactDuration, CalendarDuration, InstantInterval, and ZonedDateTimeInterval. Every function validates exact enumerable keys. Runtime identity, prototypes, and instanceof are not used for values.

ZonedDateTime stores only an epoch millisecond and a zone identifier. Local fields and offsets are derived from current host time-zone data.

Intervals and boundaries

InstantInterval and ZonedDateTimeInterval are half-open ranges [start, end). The end is excluded, so [a, b) and [b, c) are adjacent rather than overlapping, and start === end is an empty range. A reversed range rejects, and both endpoints of a zoned range must resolve to the same normalized zone.

Containment, overlap, and duration are decided on the elapsed timeline, so a zoned day that crosses a daylight-saving change measures 23 or 25 hours rather than 24. Values in a different zone can still be compared against a range.

startOfPlainDateTimeUnit, endOfPlainDateTimeUnit, and their ZonedDateTime counterparts truncate to year, month, week, day, hour, minute, or second. The end is exclusive: the end of 2026-05-04 is 2026-05-05T00:00:00.000, which is what makes zonedDateTimeUnitInterval cover the unit exactly. Weeks start on Monday unless weekStart says otherwise (ISO numbering, Sunday is 7).

Zoned boundaries resolve the truncated wall time back through the zone. Local midnight does not exist on every calendar day, so a zone that skips it rejects by default and needs an explicit disambiguation.

instantIntervalIntersection and zonedDateTimeIntervalIntersection return the shared part of two ranges, or undefined when they share no instant. instantIntervalGap and zonedDateTimeIntervalGap return the range strictly between two disjoint ranges, or undefined when they overlap or merely touch; argument order does not matter. Because [a, b) and [b, c) meet at a point both exclude, touching ranges intersect in nothing and have no gap rather than producing an empty range at b. The zoned results carry the zone of the left argument.

instantIntervalDifference and zonedDateTimeIntervalDifference return the part of the left range the right one does not cover, ordered by start. Removing the middle of a range splits it, so the result holds two ranges; removing an end or nothing gives one, and removing everything gives none. Subtracting a touching range removes nothing, since the two share no instant.

mergeInstantIntervals and mergeZonedDateTimeIntervals reduce a list to the fewest ranges covering the same instants, ordered by start. Touching ranges merge, since [a, b) and [b, c) cover exactly [a, c) and keeping them apart would describe one span with two records. Empty ranges cover no instant and drop out, so merging only empty ranges returns an empty list. Every zoned range passed to mergeZonedDateTimeIntervals must already share one zone; merging across zones would leave the zone of the result depending on sort order, so it rejects instead. Convert with withTimeZone first.

clampInstant and clampZonedDateTime take inclusive minimum and maximum bounds, not an interval record. clampZonedDateTime keeps the zone of the clamped value. minimumInstant, maximumInstant, and their zoned counterparts order by elapsed time and keep the first value on a tie.

Parsing profiles

  • parseInstant: uppercase RFC 3339-style ISO timestamp with Z or a numeric offset.
  • parsePlainDate, parsePlainTime, parsePlainDateTime: ISO calendar fields without a zone.
  • parseZonedDateTime: ISO date-time, required numeric offset, and exactly one [IANA/Zone] or fixed-offset annotation.

The zoned grammar is an intentional RFC 9557 subset. Calendar, critical, unknown, and duplicate annotations are rejected. Leap seconds and 24:00 are not supported.

Time-zone behavior

Ambiguous times, nonexistent times, calendar overflow, and offset mismatch reject by default. Callers may explicitly select Temporal-aligned policy names such as earlier, later, compatible, or constrain.

Named-zone operations support years 1970–9999. Plain, UTC, and fixed-offset values support years 0000–9999. Zone rules come from the host's ICU/tzdb version, so different runtimes can disagree on historical or future political rules. The package does not bundle or pin a zone database.

Errors

Domain failures throw DateTimeError with a stable code. When ESM and CommonJS are loaded in the same process, use isDateTimeError(error) and error.code; cross-build instanceof is not a supported contract.

Supported environments

  • Node.js 22 or newer.
  • Current Chromium, Firefox, and WebKit engines covered by the pinned Playwright release.

License

MIT © 2026 Erik Karwatowski