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

@brandonwie/dayjs-util

v0.4.0

Published

Timezone-safe date utility wrapper for dayjs — built for calendar applications

Downloads

55

Readme

@brandonwie/dayjs-util

npm version License: MIT Bundle Size

English | 한국어

Timezone-safe date utility wrapper for dayjs — built for calendar applications.

Why not just use dayjs directly?

| Concern | Raw dayjs | This library | | --------------------------- | ----------------------------------------------- | ---------------------------------------------------------- | | Timezone ambiguity | dayjs("2025-01-01") — UTC? Local? Seoul? | DayjsUtil.tzParse("2025-01-01", "Asia/Seoul") — explicit | | All-day vs timed events | No built-in distinction | stripTimezoneToUTC() vs convertToUTCDate() | | Plugin setup | Must remember extend(utc), extend(timezone) | Auto-loaded once at import | | Return type clarity | Everything returns Dayjs | *Date → JS Date, *String → string, bare → Dayjs |

Install

pnpm add @brandonwie/dayjs-util dayjs
# or
npm install @brandonwie/dayjs-util dayjs

dayjs is a peer dependency — you control the version.

Table of Contents

Quick Start

import { DayjsUtil } from "@brandonwie/dayjs-util";

// Current time in any timezone
const now = DayjsUtil.now("America/New_York");

// Parse a date AS being in a specific timezone
const seoulMidnight = DayjsUtil.tzParse("2025-01-01 00:00:00", "Asia/Seoul");
// → Dayjs representing 2025-01-01 00:00:00 KST (2024-12-31 15:00:00 UTC)

// Convert to UTC Date for database storage
const utcDate = DayjsUtil.convertToUTCDate("2025-06-15T09:00:00+09:00");
// → Date(2025-06-15T00:00:00.000Z)

// All-day events: preserve time, strip timezone
const allDay = DayjsUtil.stripTimezoneToUTC("2025-06-15T00:00:00+09:00");
// → Date(2025-06-15T00:00:00.000Z)  ← time preserved!

// Format for API responses
DayjsUtil.formatUTCString(new Date()); // "2025-06-15T00:00:00Z"
DayjsUtil.formatISOString(new Date(), "Asia/Seoul"); // "2025-06-15T09:00:00+09:00"

API Reference

Parsing

| Method | Returns | Description | | ---------------------------- | ------- | ----------------------------------------------------- | | utc(date?) | Dayjs | Create/convert to UTC | | tz(date?, timezone?) | Dayjs | Convert TO timezone (same instant, different display) | | tzParse(str, timezone?) | Dayjs | Parse AS timezone (different instant!) | | parseToTz(str?, timezone?) | Dayjs | Parse string, display in timezone |

tz() vs tzParse() — the critical difference

const str = "2025-01-01 00:00:00";

// tz(): parses in server timezone, converts display to Seoul
DayjsUtil.tz(str, "Asia/Seoul").toDate();
// → 2025-01-01T00:00:00.000Z (if server is UTC)

// tzParse(): interprets the string AS Seoul time
DayjsUtil.tzParse(str, "Asia/Seoul").toDate();
// → 2024-12-31T15:00:00.000Z (9 hours earlier!)

Use tzParse() when processing user input in their timezone. Use tz() when converting a known UTC instant for display.

// utc(): create a Dayjs object in UTC
DayjsUtil.utc("2025-06-15T09:00:00+09:00");
// → Dayjs representing 2025-06-15T00:00:00Z

DayjsUtil.utc(); // current time in UTC

// parseToTz(): parse a string and display in timezone
DayjsUtil.parseToTz("2025-06-15T00:00:00Z", "Asia/Seoul");
// → Dayjs displaying as 2025-06-15T09:00:00+09:00

Current Time

| Method | Returns | Description | | ---------------- | ------- | ---------------------------------------- | | now(timezone?) | Dayjs | Current moment in the specified timezone |

DayjsUtil.now(); // current time in UTC
DayjsUtil.now("Asia/Seoul"); // current time in Seoul
DayjsUtil.now("America/New_York"); // current time in New York

Conversion

| Method | Returns | Description | | -------------------------- | ------- | ----------------------------------------------------- | | convertToUTCDate(date?) | Date | Timezone conversion → UTC. For timed events. | | stripTimezoneToUTC(str?) | Date | Preserve time, set tz to UTC. For all-day events. | | epoch() | Date | Returns 1970-01-01T00:00:00Z. Sentinel value. |

// convertToUTCDate: proper timezone conversion
DayjsUtil.convertToUTCDate("2025-06-15T15:00:00+09:00");
// → Date(2025-06-15T06:00:00.000Z)

// stripTimezoneToUTC: keep the wall-clock time, force UTC
DayjsUtil.stripTimezoneToUTC("2025-06-15T00:00:00+09:00");
// → Date(2025-06-15T00:00:00.000Z)  ← time preserved, offset discarded

// epoch: sentinel value for "not applicable" states
DayjsUtil.epoch();
// → Date(1970-01-01T00:00:00.000Z)

Arithmetic

| Method | Returns | Description | | ---------------------------------------- | ------- | ------------------------------------ | | add(date, value, unit, timezone?) | Dayjs | Add time to a date (DST-safe) | | subtract(date, value, unit, timezone?) | Dayjs | Subtract time from a date (DST-safe) |

DST-safe: Adding 1 day across a DST boundary yields the same wall-clock time the next day, not exactly 24 hours later. Operations are performed in the specified timezone to ensure correctness.

// Add days
DayjsUtil.add("2025-06-15T09:00:00Z", 3, "day");
// → 2025-06-18T09:00:00Z

// Month-end auto-clamp: Jan 31 + 1 month = Feb 28 (not Mar 3)
DayjsUtil.add("2025-01-31T00:00:00Z", 1, "month");
// → 2025-02-28T00:00:00Z

// Subtract with timezone
DayjsUtil.subtract("2025-06-15T09:00:00Z", 2, "hour", "Asia/Seoul");
// → Dayjs representing 2025-06-15T16:00:00 KST (was 18:00 KST)

// Add hours
DayjsUtil.add("2025-06-15T09:00:00Z", 5, "hour");
// → 2025-06-15T14:00:00Z

Boundaries

| Method | Returns | Description | | ------------------------------------ | ------- | ------------------------------ | | startOf(date, unit, timezone?) | Dayjs | Start of time unit in timezone | | endOf(date, unit, timezone?) | Dayjs | End of time unit in timezone | | startOfDate(date, unit, timezone?) | Date | Start of time unit as JS Date | | endOfDate(date, unit, timezone?) | Date | End of time unit as JS Date |

Timezone matters: startOf("day") computes midnight in the specified timezone, not UTC midnight. This prevents the common bug where a Seoul user sees events from the wrong day.

// Midnight in Seoul (not UTC midnight!)
DayjsUtil.startOf("2025-06-15T02:00:00Z", "day", "Asia/Seoul");
// → 2025-06-15 00:00:00 KST (= 2025-06-14T15:00:00Z)

// Midnight in UTC
DayjsUtil.startOf("2025-06-15T02:00:00Z", "day");
// → 2025-06-15 00:00:00 UTC

// End of month
DayjsUtil.endOf("2025-06-15T00:00:00Z", "month");
// → 2025-06-30T23:59:59.999Z

// Start of week as JS Date (for database queries)
DayjsUtil.startOfDate("2025-06-15T12:00:00Z", "week", "America/New_York");
// → Date representing start of that week in Eastern time

// End of day as JS Date
DayjsUtil.endOfDate("2025-06-15T00:00:00Z", "day", "Asia/Seoul");
// → Date representing 2025-06-15T23:59:59.999 KST

Formatting

| Method | Returns | Description | | ----------------------------------- | -------- | ------------------------------- | | formatISOString(date?, tz?) | string | 2025-01-01T09:00:00+09:00 | | formatUTCString(date?) | string | 2025-01-01T00:00:00Z | | formatDateOnlyString(date?, tz?) | string | 2025-01-01 (timezone-aware) | | extractDateOnlyString(date?) | string | 2025-01-01 (no tz conversion) | | formatString(date, template, tz?) | string | Custom template with timezone |

const date = "2025-06-15T00:00:00Z";

// ISO with timezone offset
DayjsUtil.formatISOString(date, "Asia/Seoul");
// → "2025-06-15T09:00:00+09:00"

// UTC string
DayjsUtil.formatUTCString(date);
// → "2025-06-15T00:00:00Z"

// Date only (timezone-aware — the date may differ across timezones)
DayjsUtil.formatDateOnlyString("2025-06-14T23:00:00Z", "Asia/Seoul");
// → "2025-06-15" (it's already June 15 in Seoul)

// Extract date without timezone conversion (for all-day events)
DayjsUtil.extractDateOnlyString("2025-06-15T09:00:00+09:00");
// → "2025-06-15" (takes the date as-is from the string)

// Custom format template
DayjsUtil.formatString("2025-06-15T00:00:00Z", "YYYY/MM/DD");
// → "2025/06/15"

DayjsUtil.formatString("2025-06-15T00:00:00Z", "HH:mm", "Asia/Seoul");
// → "09:00"

DayjsUtil.formatString(
  "2025-06-15T00:00:00Z",
  "ddd, MMM D, YYYY",
  "America/New_York",
);
// → "Sat, Jun 14, 2025" (still June 14 in New York)

Timestamps

| Method | Returns | Description | | --------------------------- | -------- | ----------------------------- | | toUnixSeconds(date?) | number | Seconds since Unix epoch | | toUnixMilliseconds(date?) | number | Milliseconds since Unix epoch |

DayjsUtil.toUnixSeconds("2025-06-15T00:00:00Z");
// → 1750032000

DayjsUtil.toUnixMilliseconds("2025-06-15T00:00:00Z");
// → 1750032000000

// Current time
DayjsUtil.toUnixSeconds(); // seconds since epoch (now)

Duration

| Method | Returns | Description | | ------------------------------------ | -------- | ----------------------------------------- | | formatDurationString(ms, options?) | string | Human-readable duration from milliseconds |

// Short format (default)
DayjsUtil.formatDurationString(9_000_000); // "2h 30min"
DayjsUtil.formatDurationString(3_600_000); // "1h"
DayjsUtil.formatDurationString(1_800_000); // "30min"

// Long format
DayjsUtil.formatDurationString(9_000_000, { short: false });
// → "2 hours 30 minutes"

DayjsUtil.formatDurationString(3_600_000, { short: false });
// → "1 hour"

// Edge cases
DayjsUtil.formatDurationString(0); // "0min"
DayjsUtil.formatDurationString(-3_600_000); // "1h" (sign dropped)

Comparison

| Method | Returns | Description | | ------------------------------------------------------------- | --------- | -------------------------------------- | | isSame(d1?, d2?, unit?, timezone?) | boolean | Compare two dates at given granularity | | diff(d1?, d2?, unit?, timezone?) | number | Difference in specified unit | | isBefore(d1?, d2?, unit?, timezone?) | boolean | Check if d1 is before d2 | | isAfter(d1?, d2?, unit?, timezone?) | boolean | Check if d1 is after d2 | | isSameOrBefore(d1?, d2?, unit?, timezone?) | boolean | Check if d1 is same or before d2 | | isSameOrAfter(d1?, d2?, unit?, timezone?) | boolean | Check if d1 is same or after d2 | | isBetween(date, start, end, unit?, inclusivity?, timezone?) | boolean | Check if date falls within range |

// isSame: compare at different granularities
DayjsUtil.isSame("2025-06-15T10:00:00Z", "2025-06-15T22:00:00Z", "day");
// → true (same day)

DayjsUtil.isSame("2025-06-15T10:00:00Z", "2025-06-15T22:00:00Z", "hour");
// → false

// diff: calculate difference
DayjsUtil.diff("2025-06-20", "2025-06-15", "day");
// → 5

DayjsUtil.diff("2025-06-15T10:00:00Z", "2025-06-15T08:00:00Z", "hour");
// → 2

// isBefore / isAfter
DayjsUtil.isBefore("2025-06-14", "2025-06-15"); // true
DayjsUtil.isBefore("2025-06-15T23:00", "2025-06-15T01:00", "day"); // false (same day)

DayjsUtil.isAfter("2025-06-16", "2025-06-15"); // true

// isSameOrBefore / isSameOrAfter
DayjsUtil.isSameOrBefore("2025-06-15", "2025-06-15"); // true
DayjsUtil.isSameOrAfter("2025-06-15", "2025-06-14"); // true

// isBetween: range check with bracket notation
DayjsUtil.isBetween("2025-06-15", "2025-06-01", "2025-06-30", "day", "[]");
// → true (inclusive on both ends)

DayjsUtil.isBetween("2025-06-15", "2025-06-15", "2025-06-20");
// → false (default "()" excludes boundaries)

DayjsUtil.isBetween("2025-06-15", "2025-06-15", "2025-06-20", null, "[]");
// → true (inclusive — to set inclusivity without unit, pass null for unit)

Bracket notation (isBetween)

| Notation | Start | End | Meaning | | -------- | --------- | --------- | ------------------------------ | | () | exclusive | exclusive | Default | | [] | inclusive | inclusive | Both boundaries match | | [) | inclusive | exclusive | Start matches, end does not | | (] | exclusive | inclusive | Start does not match, end does |

Time Manipulation

| Method | Returns | Description | | ------------------------------------- | --------- | --------------------------------------------------- | | copyTime(source, target, timezone?) | Dayjs | Copy time from one date onto another's calendar day | | isMidnight(date, timezone?) | boolean | Check if time is exactly 00:00:00.000 |

// copyTime: drag-and-drop event to a new day while keeping its time
DayjsUtil.copyTime("2025-06-15T14:30:00Z", "2025-06-20T00:00:00Z");
// → 2025-06-20T14:30:00Z (June 20 with 14:30 from June 15)

// With timezone: preserves wall-clock time in that timezone
DayjsUtil.copyTime(
  "2025-06-15T14:30:00+09:00",
  "2025-06-20T00:00:00+09:00",
  "Asia/Seoul",
);
// → Dayjs representing 2025-06-20 14:30:00 KST

// isMidnight: all-day event detection
DayjsUtil.isMidnight("2025-06-15T00:00:00Z"); // true
DayjsUtil.isMidnight("2025-06-15T09:00:00Z"); // false
DayjsUtil.isMidnight("2025-06-14T15:00:00Z", "Asia/Seoul"); // true (midnight KST)
DayjsUtil.isMidnight("2025-06-15T00:00:00Z", "Asia/Seoul"); // false (09:00 KST)

Calendar Domain

| Method | Returns | Description | | ------------------------------------ | ---------- | ---------------------------------------- | | dayOfWeekString(date, timezone?) | RRuleDay | RRULE day-of-week code (SU, MO, ... SA) | | remainingDays(from, to, timezone?) | number | Remaining whole days (rounds up partial) |

// dayOfWeekString: get RRULE day code
DayjsUtil.dayOfWeekString("2025-06-16T00:00:00Z"); // "MO" (Monday)
DayjsUtil.dayOfWeekString("2025-06-15T00:00:00Z"); // "SU" (Sunday)
DayjsUtil.dayOfWeekString("2025-06-20T00:00:00Z"); // "FR" (Friday)

// RRULE codes: "SU" | "MO" | "TU" | "WE" | "TH" | "FR" | "SA"

// remainingDays: rounds up partial days
DayjsUtil.remainingDays("2025-06-15", "2025-06-20"); // 5
DayjsUtil.remainingDays("2025-06-15", "2025-06-15T01:00:00Z"); // 1 (partial → rounds up)
DayjsUtil.remainingDays("2025-06-15", "2025-06-15"); // 0 (exact same moment)
DayjsUtil.remainingDays("2025-06-20", "2025-06-15"); // -5 (negative = past)

Validation

| Method | Returns | Description | | -------------------------------- | --------- | --------------------------------------------- | | isValidDateFormat(str, format) | boolean | Validate string against DATE_FORMAT pattern |

import { DayjsUtil, DATE_FORMAT } from "@brandonwie/dayjs-util";

DayjsUtil.isValidDateFormat("2025-06-15", DATE_FORMAT.DATE); // true
DayjsUtil.isValidDateFormat("2025-06-15T10:00:00Z", DATE_FORMAT.DATETIME_UTC); // true
DayjsUtil.isValidDateFormat(
  "2025-06-15T10:00:00+09:00",
  DATE_FORMAT.DATETIME_OFFSET,
); // true
DayjsUtil.isValidDateFormat("not-a-date", DATE_FORMAT.DATE); // false

Supported DATE_FORMAT constants

| Constant | Pattern | Example | | -------------------- | ------------------------------- | ------------------------------- | | DATE | YYYY-MM-DD | 2025-01-01 | | DATETIME | YYYY-MM-DDTHH:mm:ss | 2025-01-01T10:00:00 | | DATETIME_UTC | YYYY-MM-DDTHH:mm:ssZ | 2025-01-01T10:00:00Z | | DATETIME_OFFSET | YYYY-MM-DDTHH:mm:ss±HH:mm | 2025-01-01T10:00:00+09:00 | | DATETIME_MS | YYYY-MM-DDTHH:mm:ss.SSS | 2025-01-01T10:00:00.000 | | DATETIME_MS_UTC | YYYY-MM-DDTHH:mm:ss.SSSZ | 2025-01-01T10:00:00.000Z | | DATETIME_MS_OFFSET | YYYY-MM-DDTHH:mm:ss.SSS±HH:mm | 2025-01-01T10:00:00.000+09:00 |

EventDateHandler (Calendar Events)

Optional import — only adds ~7KB when used. Safe to ignore if you don't work with calendar data.

Calendar APIs (Google, Microsoft, iCal) send event dates in different formats. EventDateHandler normalizes them to UTC Date objects for consistent storage.

The two kinds of calendar events

| Kind | What it means | Example | Goal | | ------- | ----------------------------------- | ------------------------------ | ---------------------------------- | | All-day | A calendar date, not a moment | "June 15th" (no specific time) | Store as midnight UTC on that date | | Timed | A specific moment in a timezone | "June 15, 9:00 AM Seoul time" | Convert to UTC point-in-time |

Import

import { EventDateHandler } from "@brandonwie/dayjs-util/event";
// or: import { EventDateHandler } from "@brandonwie/dayjs-util";

toAllDayUTC(start, end)

Extracts the date from any format and returns midnight UTC. Time and timezone in the input are ignored — only the date matters.

// All of these produce the same result:
EventDateHandler.toAllDayUTC("2025-06-15", "2025-06-16"); // Google Calendar
EventDateHandler.toAllDayUTC(
  "2025-06-15T00:00:00+09:00",
  "2025-06-16T00:00:00+09:00",
); // offset string
EventDateHandler.toAllDayUTC("2025-06-15T00:00:00Z", "2025-06-16T00:00:00Z"); // UTC
EventDateHandler.toAllDayUTC("2025-06-15T00:00:00", "2025-06-16T00:00:00"); // Microsoft Graph
EventDateHandler.toAllDayUTC("20250615", "20250616"); // iCal compact
// → { start: Date(2025-06-15T00:00:00Z), end: Date(2025-06-16T00:00:00Z), timezone: "UTC" }

toTimedUTC(start, end, timezone)

Converts a timed event to UTC. The timezone parameter is used for parsing when the input string has no embedded offset.

// String WITH offset — offset used for conversion, timezone stored as metadata
EventDateHandler.toTimedUTC(
  "2025-06-15T09:00:00+09:00",
  "2025-06-15T10:00:00+09:00",
  "Asia/Seoul",
);
// → { start: Date(2025-06-15T00:00:00Z), end: Date(2025-06-15T01:00:00Z), timezone: "Asia/Seoul" }

// String WITHOUT offset — timezone used to interpret the time
EventDateHandler.toTimedUTC(
  "2025-06-15T09:00:00",
  "2025-06-15T10:00:00",
  "Asia/Seoul",
);
// → { start: Date(2025-06-15T00:00:00Z), end: Date(2025-06-15T01:00:00Z), timezone: "Asia/Seoul" }

// UTC string — Z does the work
EventDateHandler.toTimedUTC(
  "2025-06-15T00:00:00Z",
  "2025-06-15T01:00:00Z",
  "UTC",
);
// → { start: Date(2025-06-15T00:00:00Z), end: Date(2025-06-15T01:00:00Z), timezone: "UTC" }

normalize(params)

Dispatches to toAllDayUTC or toTimedUTC based on the isAllDay flag. Returns null when start or end is missing.

// Timed event (default when isAllDay is omitted)
const result = EventDateHandler.normalize({
  start: "2025-06-15T09:00:00+09:00",
  end: "2025-06-15T10:00:00+09:00",
  timezone: "Asia/Seoul",
});
// → { start: Date(...), end: Date(...), timezone: "Asia/Seoul" }

// All-day event
const allDay = EventDateHandler.normalize({
  start: "2025-06-15",
  end: "2025-06-16",
  timezone: "Asia/Seoul",
  isAllDay: true,
});
// → { start: Date(2025-06-15T00:00:00Z), end: Date(2025-06-16T00:00:00Z), timezone: "UTC" }

// Missing dates
EventDateHandler.normalize({
  start: undefined,
  end: undefined,
  timezone: "UTC",
});
// → null

Supported input formats

| Format | Example | All-day | Timed | Source | | ----------------- | ----------------------------- | ------- | ----- | ------------------------ | | Plain date | "2025-06-15" | Yes | Yes* | Google Calendar, Cal.com | | Compact iCal | "20250615" | Yes | Yes* | RFC 5545 VALUE=DATE | | Datetime + offset | "2025-06-15T09:00:00+09:00" | Yes | Yes | Google Calendar | | Datetime UTC | "2025-06-15T09:00:00Z" | Yes | Yes | Calendly | | Datetime bare | "2025-06-15T09:00:00" | Yes | Yes | Microsoft Graph, iCal | | Date object | new Date(...) | Yes | Yes | Any |

*Date-only strings in timed events are treated as midnight in the given timezone.

API summary

| Method | Returns | Description | | ---------------------------- | ------------------------------ | ---------------------------- | | toAllDayUTC(start, end) | NormalizedEventDates | Extract date → midnight UTC | | toTimedUTC(start, end, tz) | NormalizedEventDates | Convert to UTC point-in-time | | normalize(params) | NormalizedEventDates \| null | Dispatch by isAllDay flag |

Migration Guide: new Date() → DayjsUtil

| Before | After | Why | | ---------------------------------- | --------------------------------------- | ----------------------------------- | | new Date() | DayjsUtil.utc().toDate() | Explicit UTC, no local tz ambiguity | | new Date(str) | DayjsUtil.utc(str).toDate() | Consistent parsing | | new Date(str).toISOString() | DayjsUtil.formatUTCString(str) | Same result, cleaner API | | date.toISOString().split('T')[0] | DayjsUtil.extractDateOnlyString(date) | Handles all input types | | new Date(0) | DayjsUtil.epoch() | Self-documenting sentinel | | Date.now() | DayjsUtil.now().valueOf() | Timezone-aware "now" | | d1.getTime() - d2.getTime() | DayjsUtil.diff(d1, d2, 'ms') | Readable, unit-aware | | d1 < d2 | DayjsUtil.isBefore(d1, d2) | Explicit, unit-aware | | d1 >= d2 | DayjsUtil.isSameOrAfter(d1, d2) | Timezone-safe | | Manual offset math | DayjsUtil.tz(date, 'Asia/Seoul') | IANA timezone, DST-safe | | dayjs(str).tz(tz) | DayjsUtil.tzParse(str, tz) | Correct semantics (see above) | | dayjs(d).add(1, 'day') | DayjsUtil.add(d, 1, 'day', tz) | DST-safe, timezone explicit | | dayjs(d).startOf('day') | DayjsUtil.startOf(d, 'day', tz) | Midnight in correct timezone | | Math.floor(d / 1000) | DayjsUtil.toUnixSeconds(d) | Clean API | | date.getHours() === 0 && ... | DayjsUtil.isMidnight(date, tz) | Timezone-aware midnight check |

Design Decisions

  • Static class — no instantiation needed, dayjs instances created per-call (immutable, ~0.01ms)
  • Plugins loaded once — utc, timezone, isSameOrAfter, isSameOrBefore, isBetween, duration registered at import time
  • Peer dependency on dayjs — consumers control the version, no duplication
  • Dual CJS/ESM — works in Node.js, browsers, and bundlers
  • Tree-shakeablesideEffects: false for optimal bundling; EventDateHandler can be imported separately via /event entry point

How DST is Handled

This library is DST-safe because every timezone-aware method applies the timezone before performing operations. The actual DST resolution is delegated down a chain:

DayjsUtil → dayjs timezone plugin → Intl.DateTimeFormat → OS/ICU timezone data

This library has no built-in timezone database. It relies on the JavaScript runtime's Intl.DateTimeFormat API, which reads DST rules from the operating system's copy of the IANA timezone database.

Why the timezone parameter matters for DST

The offset in an ISO 8601 string (e.g., -05:00) resolves the input to an exact UTC instant. The timezone parameter tells arithmetic operations which calendar/clock rules to follow. These are different jobs:

// Frontend sends a New York time with offset (EST = -05:00)
DayjsUtil.add("2025-03-09T01:00:00-05:00", 1, "day", "America/New_York");
// → March 10, 01:00 AM EDT (-04:00, DST kicked in)
// → Internally: 2025-03-10T05:00:00Z (23 real hours, not 24)

// Without timezone: raw +24h math
DayjsUtil.add("2025-03-09T01:00:00-05:00", 1, "day");
// → March 10, 02:00 AM EDT (wall-clock drifted by 1 hour)

For a calendar showing "daily event at 1:00 AM", that 1-hour drift matters.

Which methods are DST-aware

| Method | Why DST matters | | --------------------------------- | ------------------------------------------------------------ | | add / subtract | Wall-clock time preserved across DST boundaries | | startOf / endOf | Midnight depends on the timezone's DST state | | isSame / isBefore / isAfter | "Same day" boundary shifts with DST | | remainingDays | Uses calendar-day diff, not ms / 86400000 | | toTimedUTC (EventDateHandler) | dayjs.tz() resolves DST when parsing bare datetime strings |

Edge cases

| Case | Behavior | | ------------------------------------------------------------------ | ---------------------------------------------------------------------------- | | Ambiguous time (fall-back overlap, e.g., 1:30 AM occurs twice) | dayjs picks the first occurrence (pre-transition) | | Non-existent time (spring-forward gap, e.g., 2:30 AM skipped) | dayjs rolls forward to the next valid time | | Stale timezone data | Requires updating Node.js or OS; this library does not ship its own database |

Note: Timezones that do not observe DST (e.g., Asia/Seoul, UTC) are unaffected. The timezone parameter still works — it just applies a fixed offset.

Breaking Changes in v0.4.0

EventDateHandler API redesigned

| v0.3.x | v0.4.0 | Notes | | ---------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------- | | processAllDayEventDates(start, end) | toAllDayUTC(start, end) | Accepts DateInput (not just string). Always returns midnight. | | processTimedEventDates(start, end, tz) | toTimedUTC(start, end, tz) | tz now used for parsing offset-less strings. | | computeScheduleDates(params) | normalize(params) | Params: start/end/timezone (not startAt/endAt/timeZone). Returns object or null. |

// v0.3.x
const { startAt, endAt, zone } = EventDateHandler.processAllDayEventDates(
  start,
  end,
);
const [s, e, z] = EventDateHandler.computeScheduleDates({
  startAt,
  endAt,
  timeZone,
  isAllDay,
});

// v0.4.0
const { start, end, timezone } = EventDateHandler.toAllDayUTC(
  startDate,
  endDate,
);
const result = EventDateHandler.normalize({ start, end, timezone, isAllDay });
if (result) {
  const { start, end, timezone } = result;
}

References

License

MIT