You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
import{DayjsUtil}from"@brandonwie/dayjs-util";// Current time in any timezoneconstnow=DayjsUtil.now("America/New_York");// Parse a date AS being in a specific timezoneconstseoulMidnight=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 storageconstutcDate=DayjsUtil.convertToUTCDate("2025-06-15T09:00:00+09:00");// → Date(2025-06-15T00:00:00.000Z)// All-day events: preserve time, strip timezoneconstallDay=DayjsUtil.stripTimezoneToUTC("2025-06-15T00:00:00+09:00");// → Date(2025-06-15T00:00:00.000Z) ← time preserved!// Format for API responsesDayjsUtil.formatUTCString(newDate());// "2025-06-15T00:00:00Z"DayjsUtil.formatISOString(newDate(),"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
conststr="2025-01-01 00:00:00";// tz(): parses in server timezone, converts display to SeoulDayjsUtil.tz(str,"Asia/Seoul").toDate();// → 2025-01-01T00:00:00.000Z (if server is UTC)// tzParse(): interprets the string AS Seoul timeDayjsUtil.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 UTCDayjsUtil.utc("2025-06-15T09:00:00+09:00");// → Dayjs representing 2025-06-15T00:00:00ZDayjsUtil.utc();// current time in UTC// parseToTz(): parse a string and display in timezoneDayjsUtil.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 UTCDayjsUtil.now("Asia/Seoul");// current time in SeoulDayjsUtil.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 conversionDayjsUtil.convertToUTCDate("2025-06-15T15:00:00+09:00");// → Date(2025-06-15T06:00:00.000Z)// stripTimezoneToUTC: keep the wall-clock time, force UTCDayjsUtil.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" statesDayjsUtil.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 daysDayjsUtil.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 timezoneDayjsUtil.subtract("2025-06-15T09:00:00Z",2,"hour","Asia/Seoul");// → Dayjs representing 2025-06-15T16:00:00 KST (was 18:00 KST)// Add hoursDayjsUtil.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 UTCDayjsUtil.startOf("2025-06-15T02:00:00Z","day");// → 2025-06-15 00:00:00 UTC// End of monthDayjsUtil.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 DateDayjsUtil.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
constdate="2025-06-15T00:00:00Z";// ISO with timezone offsetDayjsUtil.formatISOString(date,"Asia/Seoul");// → "2025-06-15T09:00:00+09:00"// UTC stringDayjsUtil.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 templateDayjsUtil.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");// → 1750032000DayjsUtil.toUnixMilliseconds("2025-06-15T00:00:00Z");// → 1750032000000// Current timeDayjsUtil.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 formatDayjsUtil.formatDurationString(9_000_000,{short: false});// → "2 hours 30 minutes"DayjsUtil.formatDurationString(3_600_000,{short: false});// → "1 hour"// Edge casesDayjsUtil.formatDurationString(0);// "0min"DayjsUtil.formatDurationString(-3_600_000);// "1h" (sign dropped)
// isSame: compare at different granularitiesDayjsUtil.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 differenceDayjsUtil.diff("2025-06-20","2025-06-15","day");// → 5DayjsUtil.diff("2025-06-15T10:00:00Z","2025-06-15T08:00:00Z","hour");// → 2// isBefore / isAfterDayjsUtil.isBefore("2025-06-14","2025-06-15");// trueDayjsUtil.isBefore("2025-06-15T23:00","2025-06-15T01:00","day");// false (same day)DayjsUtil.isAfter("2025-06-16","2025-06-15");// true// isSameOrBefore / isSameOrAfterDayjsUtil.isSameOrBefore("2025-06-15","2025-06-15");// trueDayjsUtil.isSameOrAfter("2025-06-15","2025-06-14");// true// isBetween: range check with bracket notationDayjsUtil.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 timeDayjsUtil.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 timezoneDayjsUtil.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 detectionDayjsUtil.isMidnight("2025-06-15T00:00:00Z");// trueDayjsUtil.isMidnight("2025-06-15T09:00:00Z");// falseDayjsUtil.isMidnight("2025-06-14T15:00:00Z","Asia/Seoul");// true (midnight KST)DayjsUtil.isMidnight("2025-06-15T00:00:00Z","Asia/Seoul");// false (09:00 KST)
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 CalendarEventDateHandler.toAllDayUTC("2025-06-15T00:00:00+09:00","2025-06-16T00:00:00+09:00",);// offset stringEventDateHandler.toAllDayUTC("2025-06-15T00:00:00Z","2025-06-16T00:00:00Z");// UTCEventDateHandler.toAllDayUTC("2025-06-15T00:00:00","2025-06-16T00:00:00");// Microsoft GraphEventDateHandler.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 metadataEventDateHandler.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 timeEventDateHandler.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 workEventDateHandler.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.
*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-shakeable — sideEffects: false for optimal bundling; EventDateHandler can be imported separately via /event entry point
~7.7 kB gzipped total production cost — dayjs core + 6 plugins + this library, measured with esbuild minify + gzip
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:
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 mathDayjsUtil.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.
Contributions are welcome! Please read the Contributing Guide before submitting issues or pull requests. We have issue templates for bug reports and feature requests to help you get started.
References
dayjs — lightweight date library this package wraps