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

@siwenwu/tiny-date-kit

v1.0.0

Published

Tiny, dependency-free date helpers: relative time formatting, strict ISO date validation, and timezone-safe date-only comparison.

Readme

@siwenwu/tiny-date-kit

Tiny, dependency-free date helpers for Node.js — relative time formatting, strict ISO 8601 validation, and timezone-safe date-only comparison.

No dependencies. CommonJS with TypeScript definitions. Node 18+.

Why

Three things go wrong often enough to be worth a small library:

  • new Date(string) guesses. For anything outside the ISO 8601 grammar the spec hands parsing to the engine, so the same string can mean different instants — or Invalid Date — depending on where it runs. These helpers accept only strict ISO 8601 and reject everything else.
  • "Same day" is a time zone question. Comparing getDate() reads the host's offset, which is rarely the one you meant. Every date-only function here takes an explicit IANA timeZone.
  • Day arithmetic drifts across DST. Dividing elapsed milliseconds by 86,400,000 is off by an hour twice a year. diffInDays counts day boundaries crossed instead.

Install

npm install @siwenwu/tiny-date-kit

Usage

const {
  formatRelativeTime,
  isValidISODate,
  toDateKey,
  isSameDay,
  diffInDays,
} = require('@siwenwu/tiny-date-kit');

formatRelativeTime('2024-06-12T12:00:00Z', { now: '2024-06-15T12:00:00Z' });
// => '3 days ago'

isValidISODate('2023-02-29');
// => false  (2023 is not a leap year)

toDateKey('2024-06-15T02:00:00Z', { timeZone: 'America/New_York' });
// => '2024-06-14'  (still the previous evening in New York)

isSameDay('2024-06-15T01:00:00Z', '2024-06-15T05:00:00Z', { timeZone: 'America/New_York' });
// => false  (they straddle midnight there)

diffInDays('2024-02-28', '2024-03-01', { timeZone: 'UTC' });
// => 2  (2024 has a 29 February)

Every function accepts a Date, epoch milliseconds, or a strict ISO 8601 string.

API

formatRelativeTime(value, options?)

Human-readable relative time, backed by Intl.RelativeTimeFormat.

formatRelativeTime(Date.now() - 7200000);           // => '2 hours ago'
formatRelativeTime('2024-06-16', { now: '2024-06-15' }); // => 'tomorrow'
formatRelativeTime(then, { locale: 'es' });          // => 'hace 3 días'
formatRelativeTime(then, { style: 'narrow' });       // => '3d ago'
formatRelativeTime(then, { numeric: 'always' });     // => '1 day ago', not 'yesterday'

| Option | Default | Meaning | | --- | --- | --- | | now | current time | Reference instant to measure against | | timeZone | host zone | IANA zone used for day-and-above units | | locale | host locale | BCP 47 locale, e.g. 'es', 'fr' | | numeric | 'auto' | 'auto' prefers yesterday over 1 day ago | | style | 'long' | 'long', 'short' or 'narrow' |

Gaps under 22 hours are reported in seconds, minutes or hours. At a day and above the count comes from the calendar in timeZone, so it tracks boundaries crossed rather than elapsed 24-hour periods:

// 2024-03-10 in New York is a 23-hour day, but it is still one day.
diffInDays('2024-03-09T17:00:00Z', '2024-03-10T16:00:00Z', { timeZone: 'America/New_York' });
// => 1

getRelativeTimeParts(value, options?)

The { value, unit } pair behind formatRelativeTime, for rendering your own markup. Same options. value is negative in the past; unit is one of second, minute, hour, day, month, year.

getRelativeTimeParts('2024-06-12', { now: '2024-06-15' });
// => { value: -3, unit: 'day' }

isValidISODate(value) / isValidISODateTime(value)

Strict format and calendar checks. Non-strings return false rather than being coerced.

isValidISODate('2024-02-29');            // => true
isValidISODate('2023-02-29');            // => false  (not a leap year)
isValidISODate('1900-02-29');            // => false  (century, not divisible by 400)
isValidISODate('2024-04-31');            // => false  (April has 30 days)
isValidISODate('2024-1-01');             // => false  (unpadded month)
isValidISODate('2024-01-01T00:00:00Z');  // => false  (that is a date-time)

isValidISODateTime('2024-02-29T12:30:00Z');       // => true
isValidISODateTime('2024-02-29T12:30:00+05:30');  // => true
isValidISODateTime('2024-02-29T24:00:00Z');       // => false  (hour 24)

parseISODate(value)

Parses strict ISO 8601 and returns a Date, or null — never an Invalid Date you have to test for separately. A bare YYYY-MM-DD resolves to midnight UTC, per the ECMAScript spec.

parseISODate('2024-02-29');    // => Date 2024-02-29T00:00:00.000Z
parseISODate('2023-02-29');    // => null
parseISODate('next tuesday');  // => null

toDateKey(value, options?)

The calendar date an instant falls on, as YYYY-MM-DD, in options.timeZone. The timezone-safe way to bucket timestamps by day.

const instant = '2024-06-15T02:00:00Z';
toDateKey(instant, { timeZone: 'UTC' });              // => '2024-06-15'
toDateKey(instant, { timeZone: 'America/New_York' }); // => '2024-06-14'
toDateKey(instant, { timeZone: 'Asia/Tokyo' });       // => '2024-06-15'

compareDateOnly(a, b, options?)

Compares two instants by calendar date, ignoring clock time. Returns -1, 0 or 1, so it drops straight into Array.prototype.sort.

compareDateOnly('2024-06-15T00:00:00Z', '2024-06-15T23:59:59Z', { timeZone: 'UTC' }); // => 0
compareDateOnly('2024-06-14T23:59:59Z', '2024-06-15T00:00:00Z', { timeZone: 'UTC' }); // => -1

isSameDay(a, b, options?)

compareDateOnly(a, b, options) === 0.

diffInDays(a, b, options?)

Whole calendar days from a to b, signed. Counts day boundaries crossed, so DST transitions never produce fractional or off-by-one results.

diffInDays('2024-06-14T23:30:00Z', '2024-06-15T00:30:00Z', { timeZone: 'UTC' }); // => 1
diffInDays('2024-06-15T00:01:00Z', '2024-06-15T23:59:00Z', { timeZone: 'UTC' }); // => 0
diffInDays('2024-01-01', '2025-01-01', { timeZone: 'UTC' });                     // => 366

isLeapYear(value) / daysInMonth(year, month)

isLeapYear(2024);        // => true
isLeapYear(1900);        // => false  (century, not divisible by 400)
isLeapYear(2000);        // => true
daysInMonth(2024, 2);    // => 29
daysInMonth(2023, 2);    // => 28

CLI

npx @siwenwu/tiny-date-kit rel 2024-01-01 --now 2024-01-04
# 3 days ago

npx @siwenwu/tiny-date-kit key 2024-03-10T04:30:00Z --tz America/New_York
# 2024-03-09

npx @siwenwu/tiny-date-kit valid 2023-02-29   # prints false, exits 1
npx @siwenwu/tiny-date-kit diff 2024-02-28 2024-03-01
# 2

Commands: rel, key, valid, diff, cmp. Pass --tz <zone> to set the time zone and now in place of a date for the current instant. tiny-date --help lists everything.

Errors

Invalid input throws rather than propagating NaN or Invalid Date:

formatRelativeTime('yesterday');    // TypeError: expected a strict ISO 8601 string
toDateKey(Date.now(), { timeZone: 'Mars/Olympus' }); // RangeError: not a recognized IANA time zone

The predicates (isValidISODate, isValidISODateTime) and parseISODate never throw — they return false/null.

Time zone defaults

Every timeZone option defaults to the host zone (Intl.DateTimeFormat().resolvedOptions().timeZone). That is convenient for CLIs and user-facing formatting, but pass an explicit zone on a server, where the host zone is an accident of deployment.

Testing

npm test

49 tests via the built-in node:test runner, covering leap years and century rules, DST transitions in both directions, fractional-hour zones, midnight boundaries, unit-selection thresholds, and invalid input.

License

MIT