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/tinydate-helpers

v1.0.0

Published

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

Readme

@siwenwu/tinydate-helpers

npm license

Tiny, dependency-free date helpers for the three things that keep going wrong:

  • Relative time — "3 days ago", "in 2 hours", localized via Intl.
  • Strict ISO 8601 validation — rejects 2019-02-29 and 2024-13-01 instead of quietly producing an Invalid Date.
  • Timezone-safe date-only comparison — "is this the same day?" without the UTC-midnight off-by-one-day bug.

Zero dependencies, ESM, ships with a tinydate CLI. Requires Node.js 18+.

Install

npm install @siwenwu/tinydate-helpers

Quick start

import {
  formatRelativeTime,
  isValidISODate,
  isSameDay,
  toDateOnly,
  diffInCalendarDays,
} from '@siwenwu/tinydate-helpers';

formatRelativeTime('2024-03-01', { now: '2024-03-04' });
//=> '3 days ago'

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

toDateOnly('2024-03-10T00:00:00Z', 'America/New_York');
//=> '2024-03-09'  (midnight UTC is still the 9th in New York)

isSameDay('2024-07-01T03:00:00Z', '2024-07-01T22:00:00Z', 'Asia/Tokyo');
//=> false  (Jul 1 12:00 and Jul 2 07:00 in Tokyo)

diffInCalendarDays('2024-02-28', '2024-03-01');
//=> 2  (2024 has a Feb 29; the same range in 2023 is 1)

Why the timezone-safe part matters

// The bug this library exists to prevent:
new Date('2024-03-10').getDate();       //=> 9 in any US timezone
toDateOnly('2024-03-10', 'America/New_York'); //=> '2024-03-10'

A bare YYYY-MM-DD string is treated as a floating calendar date and is never re-interpreted through a timezone. Anything with a time component is an instant, and you choose the timezone it is resolved in. Day arithmetic is done on { year, month, day } integers, so a 23-hour or 25-hour DST day still counts as exactly one day.

API

All functions are named exports. Every function that accepts a date takes a Date, epoch milliseconds, or a strict ISO 8601 string, and throws a TypeError on anything else — nothing silently returns NaN or Invalid Date.

Relative time

formatRelativeTime(value, options?)

Formats the distance between value and a reference point as a phrase.

| Option | Type | Default | Description | | --- | --- | --- | --- | | now | date | new Date() | The reference point. | | locale | string \| string[] | host locale | BCP 47 locale for Intl.RelativeTimeFormat. | | numeric | 'auto' \| 'always' | 'auto' | 'auto' prefers "yesterday" over "1 day ago". | | style | 'long' \| 'short' \| 'narrow' | 'long' | Phrase width. | | timeZone | string | host zone | IANA zone used for calendar-day boundaries. |

formatRelativeTime('2024-03-03', { now: '2024-03-04' });                    //=> 'yesterday'
formatRelativeTime('2024-03-03', { now: '2024-03-04', numeric: 'always' }); //=> '1 day ago'
formatRelativeTime('2024-03-07', { now: '2024-03-04' });                    //=> 'in 3 days'
formatRelativeTime('2024-03-01', { now: '2024-03-04', locale: 'es' });      //=> 'hace 3 días'
formatRelativeTime('2024-03-01', { now: '2024-03-04', style: 'narrow' });   //=> '3d ago'

Unit selection: seconds below 45s, minutes below 45min, hours below 22h, then calendar days, weeks, months, and years. Because units at or above a day are calendar-based, "yesterday" always means the previous calendar day in timeZone rather than "roughly 24 hours ago".

timeAgo(value, options?)

formatRelativeTime with numeric: 'always', for when you always want a number.

timeAgo('2024-03-03', { now: '2024-03-04' }); //=> '1 day ago'

ISO 8601 validation

isValidISODate(value, options?)

Returns true only for a strict ISO 8601 calendar date or date-time. Non-strings return false rather than throwing.

Accepted: YYYY-MM-DD, YYYY-MM-DDTHH:mm, …:ss, …:ss.sss (1–9 fractional digits), each optionally suffixed with Z or ±HH:mm.

| Option | Type | Default | Description | | --- | --- | --- | --- | | allowTime | boolean | true | Allow a time-of-day portion. | | requireTime | boolean | false | Require a time-of-day portion. | | requireOffset | boolean | false | Require Z or ±HH:mm. |

isValidISODate('2024-02-29');                              //=> true
isValidISODate('2023-02-29');                              //=> false
isValidISODate('1900-02-29');                              //=> false (century non-leap)
isValidISODate('2024-03-04T12:30:45.123+05:30');           //=> true
isValidISODate('2024-03-04 12:30');                        //=> false (space separator)
isValidISODate('2024-03-04T24:00:00Z');                    //=> false
isValidISODate('March 4, 2024');                           //=> false
isValidISODate('2024-03-04T12:00:00', { requireOffset: true }); //=> false

parseISODate(value, options?)

Returns a Date, or null if the input is not strictly valid. Date-only strings are read as midnight UTC.

parseISODate('2024-03-04').toISOString();                 //=> '2024-03-04T00:00:00.000Z'
parseISODate('2024-03-04T12:00:00+02:00').toISOString();  //=> '2024-03-04T10:00:00.000Z'
parseISODate('2019-02-29');                               //=> null

isLeapYear(year) · daysInMonth(year, month)

isLeapYear(2024);       //=> true
isLeapYear(1900);       //=> false
isLeapYear(2000);       //=> true
daysInMonth(2024, 2);   //=> 29
daysInMonth(2023, 2);   //=> 28

Date-only (calendar day) operations

Each of these takes an optional trailing timeZone (IANA name). Omit it to use the host timezone; pass an unknown zone and you get a RangeError.

toDateOnly(value, timeZone?)

The calendar date of value in timeZone, as YYYY-MM-DD.

toDateOnly(new Date('2024-03-10T00:00:00Z'), 'America/New_York'); //=> '2024-03-09'
toDateOnly(new Date('2024-03-10T00:00:00Z'), 'Asia/Tokyo');       //=> '2024-03-10'
toDateOnly('2024-03-10', 'America/New_York');                     //=> '2024-03-10' (floating)

compareDateOnly(a, b, timeZone?)

-1, 0, or 1 — usable directly as an Array#sort comparator.

compareDateOnly('2024-03-04T23:59:59Z', '2024-03-04T00:00:00Z', 'UTC'); //=> 0
dates.sort((a, b) => compareDateOnly(a, b, 'UTC'));

isSameDay(a, b, timeZone?)

isSameDay('2024-03-10T23:00:00Z', '2024-03-11T02:00:00Z', 'Asia/Tokyo');  //=> true
isSameDay('2024-03-10T23:00:00Z', '2024-03-11T02:00:00Z', 'UTC');        //=> false

diffInCalendarDays(a, b, timeZone?) · diffInCalendarMonths(a, b, timeZone?)

Signed whole-unit differences from a to b. Partial units are not counted, and DST transitions never add or drop a day.

diffInCalendarDays('2024-01-01', '2025-01-01');   //=> 366 (leap year)
diffInCalendarDays('2023-01-01', '2024-01-01');   //=> 365
// 47 elapsed hours across New York's spring-forward, still 2 calendar days:
diffInCalendarDays('2024-03-09T17:00:00Z', '2024-03-11T16:00:00Z', 'America/New_York'); //=> 2

diffInCalendarMonths('2024-01-15', '2024-03-14'); //=> 1 (one day short of 2)
diffInCalendarMonths('2024-01-31', '2024-02-29'); //=> 0 (not yet a full month)

toDate(value, label?)

The coercion the other functions use, exported for reuse. Returns a defensive copy and throws a TypeError on anything unusable.

toDate('2024-03-04');   //=> Date 2024-03-04T00:00:00.000Z
toDate('not a date');   // TypeError

CLI

npx @siwenwu/tinydate-helpers --help
tinydate relative <date> [--now <date>] [--locale <bcp47>] [--numeric auto|always]
                         [--style long|short|narrow] [--tz <IANA zone>]
tinydate validate <value> [--require-offset] [--no-time]
tinydate date-only <date> [--tz <IANA zone>]
tinydate diff <from> <to> [--tz <IANA zone>]
tinydate compare <a> <b> [--tz <IANA zone>]
$ tinydate relative 2024-03-01 --now 2024-03-04
3 days ago

$ tinydate date-only 2024-03-10T00:00:00Z --tz America/New_York
2024-03-09

$ tinydate diff 2024-02-28 2024-03-01
2

$ tinydate validate 2019-02-29
invalid

Exit codes: 0 success, 1 usage or input error, 2 validate reported invalid — so validate works in a shell conditional:

if tinydate validate "$INPUT" > /dev/null; then echo "ok"; fi

Development

npm test

The suite runs on Node's built-in test runner with no dev dependencies, and passes under any host timezone (verified against UTC, America/New_York, Asia/Tokyo, Pacific/Kiritimati, and the half-hour-offset Australia/Lord_Howe).

License

MIT