bookguru-time
v0.1.3
Published
Shared timezone utilities for BookGuru applications.
Downloads
135
Readme
bookguru-time
Shared timezone utilities for BookGuru appointments.
The package uses @js-temporal/polyfill
to keep appointment storage, business-local input, date-range queries, and display
formatting consistent across bookguru-v2, bookguru-main, and bookguru-staff.
Rule Of Thumb
Store exact appointment timestamps in MongoDB as Date values. MongoDB stores
those instants in UTC.
Use the business timezone only at the edges:
- Convert a date-picker wall time into UTC before saving.
- Build UTC ranges before querying a business-local calendar day.
- Convert UTC instants into the business timezone before displaying them.
Do not use the phone timezone, browser timezone, or server timezone as a fallback for appointment calculations. Read the business timezone from the business record and pass it explicitly.
Install
After publishing to npm, install it from any BookGuru app:
npm install bookguru-timeFor local development before publishing a new version, an app can install the sibling directory instead:
npm install ../bookguru-timeImport the package using:
import {
addMinutes,
dayRangeUTC,
formatZoned,
formatZonedPattern,
isAfterInstant,
isBeforeInstant,
isSameBusinessDay,
isSameInstant,
isValidZone,
setTimeLocale,
systemTimeZone,
toISO,
toUTC,
toZoned,
zonedDate,
zonedNow,
} from "bookguru-time";Use toISO() when normalizing an existing exact instant, systemTimeZone()
only for device-local fallback behavior, and setTimeLocale() when compact
pattern labels need to follow the active application locale.
Data Types
Keep these values separate:
| Type | Example | Meaning |
| --- | --- | --- |
| UTC instant | 2026-06-01T14:00:00.000Z | One exact moment used by APIs and MongoDB |
| Wall time | 2026-06-01T10:00 | Time selected on the business calendar |
| Timezone | America/Toronto | IANA timezone stored on the business record |
| Display string | Jun 1, 2026, 10:00 AM | Human-readable UI output only |
Convert A Calendar Input Before Saving
Use toUTC() only for a wall-clock value without Z or a numeric offset:
import { toUTC } from "bookguru-time";
const start = toUTC("2026-06-01T10:00", "America/Toronto");
// "2026-06-01T14:00:00.000Z"
await Appointment.create({
start: new Date(start),
});toUTC() rejects missing or duplicated DST times by default. This is safer than
silently placing an appointment in the wrong hour:
toUTC("2026-03-08T02:30", "America/Toronto");
// Throws RangeError because this wall time does not exist.If the UI asks the user which duplicated hour they intended, pass an explicit choice:
toUTC("2026-11-01T01:30", "America/Toronto", {
disambiguation: "later",
});Display An Appointment
Use formatZoned() with standard Intl.DateTimeFormat options:
import { formatZoned } from "bookguru-time";
formatZoned(
"2026-06-01T14:00:00.000Z",
"America/Toronto",
{
weekday: "short",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
},
"en-US",
);
// "Mon, Jun 1, 10:00 AM"Use toZoned() when code needs timezone-aware fields or calculations:
import { toZoned } from "bookguru-time";
const local = toZoned("2026-06-01T14:00:00.000Z", "America/Toronto");
console.log(local.hour);
// 10Use formatZonedPattern() when an existing UI already uses compact
Moment-style display patterns:
import { formatZonedPattern } from "bookguru-time";
formatZonedPattern(
"2026-06-01T14:00:00.000Z",
"America/Toronto",
"ddd, MMM D, YYYY [at] h:mm A",
);
// "Mon, Jun 1, 2026 at 10:00 AM"Query A Business Calendar Day
Use an exclusive end boundary to avoid millisecond edge cases:
import { dayRangeUTC } from "bookguru-time";
const { start, endExclusive } = dayRangeUTC(
"2026-06-01",
"America/Toronto",
);
const appointments = await Appointment.find({
businessId,
start: {
$gte: new Date(start),
$lt: new Date(endExclusive),
},
});Add Appointment Duration
import { addMinutes } from "bookguru-time";
const end = addMinutes("2026-06-01T14:00:00.000Z", 90);
// "2026-06-01T15:30:00.000Z"Compare Appointment Slots
Use explicit instant comparisons for UTC timestamps:
import {
isAfterInstant,
isBeforeInstant,
isSameInstant,
} from "bookguru-time";
isBeforeInstant(start, end);
isAfterInstant(end, start);
isSameInstant(firstSlot.start, secondSlot.start);Use isSameBusinessDay() only when the calendar date must be evaluated in a
business timezone:
import { isSameBusinessDay } from "bookguru-time";
isSameBusinessDay(start, end, "America/Toronto");Navigate A Date Picker
Use zonedDate() and zonedNow() for business-local date-picker navigation.
Keep UTC appointment arithmetic in addMinutes():
import { zonedDate, zonedNow } from "bookguru-time";
const today = zonedNow("America/Toronto").startOf("day");
const selected = zonedDate("2026-06-01", "America/Toronto");
const tomorrow = selected.clone().add(1, "day");Validate Business Timezones
import { isValidZone } from "bookguru-time";
isValidZone("America/Toronto");
// trueStore IANA timezone names such as America/Toronto, America/New_York, or
Asia/Ho_Chi_Minh. Avoid fixed offsets such as -04:00 because offsets change
when daylight-saving rules change.
Legacy Moment-Timezone Migration
The package includes a Temporal-backed time facade only for code that has not
yet been migrated away from Moment-style chains:
import { time } from "bookguru-time";
const start = time.tz("2026-06-01T10:00", "America/Toronto");
const end = start.clone().add(90, "minutes");
console.log(end.toISOString());
// "2026-06-01T15:30:00.000Z"
console.log(end.format("YYYY-MM-DD HH:mm"));
// "2026-06-01 11:30"The facade supports the operations used by the BookGuru calendar, including
clone, tz, utc, add, subtract, startOf, endOf, format, diff,
toDate, toISOString, set, isBefore, isAfter, isSame,
isSameOrBefore, isSameOrAfter, and date/time field getters and setters.
Both Moment-style singular aliases such as hour() and plural aliases such as
hours() are supported.
It also supports static UTC construction for existing slot logic:
const end = time.utc(start).add(30, "minutes").toISOString();Prefer explicit helpers such as toUTC(), formatZoned(),
formatZonedPattern(), dayRangeUTC(), and addMinutes() for new and migrated
code. The facade remains only as a temporary compatibility bridge.
API Boundary Example
Prefer sending UTC instants through APIs:
{
"start": "2026-06-01T14:00:00.000Z",
"end": "2026-06-01T15:00:00.000Z"
}If an endpoint receives date-picker values instead, make that explicit:
{
"startWallTime": "2026-06-01T10:00",
"endWallTime": "2026-06-01T11:00",
"businessTimezone": "America/Toronto"
}Do not name a wall time start if the rest of the API expects start to be a
UTC instant.
Run Tests
npm testThe suite covers normal conversion, formatting, comparisons, date-picker navigation, business-day ranges, timezone validation, elapsed-minute math, and Toronto daylight-saving transitions.
Publish To npm
Log in to npm on the publishing machine:
npm loginRun the tests and publish:
npm test
npm publishFor later releases, increment the version first:
npm version patch
npm publish