Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# @suaveplan/datetime - Timezone-aware date/time utilities with business-day arithmetic, recurrence rules, calendar math, and relative time formatting
|
||||
|
||||
## Summary
|
||||
|
||||
Timezone-aware date/time utilities covering the most common application-layer date operations that the native `Date` API leaves unaddressed. The package provides calendar math (`addDuration`, `subtractDuration`, `startOf`, `endOf`, `getCalendarMonth`, leap-year detection), business-day arithmetic with configurable holiday calendars and custom weekend definitions, an RRULE-subset recurrence engine (DAILY/WEEKLY/MONTHLY/YEARLY with BYDAY/BYMONTH/COUNT/UNTIL), and human-readable relative time formatting (`"3 hours ago"`, `"in 2 days"`). All stateful entry points accept an injectable `Clock` for deterministic testing. Use this package for scheduling UIs, calendar components, invoice date calculations, and any domain where "next business day" or "every other Monday" has real meaning. For low-level stopwatches, throttle/debounce, or vector clocks use `@suaveplan/timing`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [business-days](./src/business-days/business-days.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [business-days](./src/business-days/business-days.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [calendar-boundary](./src/calendar/calendar-boundary.md) — `calendar-boundary` provides `startOf` and `endOf` functions for `@suaveplan/datetime`, computing the precise first and…
|
||||
- [calendar-core](./src/calendar/calendar-core.md) — `calendar-core` provides the foundational date primitives for `@suaveplan/datetime`: coercing heterogeneous `DateInput`…
|
||||
- [calendar-duration](./src/calendar/calendar-duration.md) — `calendar-duration` provides duration arithmetic for `@suaveplan/datetime`: adding and subtracting `Duration` bags from…
|
||||
- [calendar-grid](./src/calendar/calendar-grid.md) — `calendar-grid` provides `getCalendarMonth`, which builds a full calendar month grid for use in UI calendar components…
|
||||
- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [datetime-error](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [datetime-otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [recurrence-generate](./src/recurrence/recurrence-generate.md) — `recurrence-generate` provides the internal generator functions that produce infinite sequences of candidate dates for…
|
||||
- [recurrence-helpers](./src/recurrence/recurrence-helpers.md) — `recurrence-helpers` provides the internal constants and low-level date arithmetic utilities shared across the recurren…
|
||||
- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [business-days](./src/business-days/business-days.md) — <!-- begin:auto-docs --> _(no submodules)_- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- [calendar-boundary](./src/calendar/calendar-boundary.md) — `calendar-boundary` provides `startOf` and `endOf` functions for `@suaveplan/datetime`, computing the precise first and…
|
||||
- [calendar-core](./src/calendar/calendar-core.md) — `calendar-core` provides the foundational date primitives for `@suaveplan/datetime`: coercing heterogeneous `DateInput`…
|
||||
- [calendar-duration](./src/calendar/calendar-duration.md) — `calendar-duration` provides duration arithmetic for `@suaveplan/datetime`: adding and subtracting `Duration` bags from…
|
||||
- [calendar-grid](./src/calendar/calendar-grid.md) — `calendar-grid` provides `getCalendarMonth`, which builds a full calendar month grid for use in UI calendar components…
|
||||
- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime-error](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime-otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_- [recurrence-generate](./src/recurrence/recurrence-generate.md) — `recurrence-generate` provides the internal generator functions that produce infinite sequences of candidate dates for…
|
||||
- [recurrence-helpers](./src/recurrence/recurrence-helpers.md) — `recurrence-helpers` provides the internal constants and low-level date arithmetic utilities shared across the recurren…
|
||||
- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_- [business-days](./src/business-days/business-days.md) — <!-- begin:auto-docs --> _(no submodules)_- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- [calendar-boundary](./src/calendar/calendar-boundary.md) — `calendar-boundary` provides `startOf` and `endOf` functions for `@suaveplan/datetime`, computing the precise first and…
|
||||
- [calendar-core](./src/calendar/calendar-core.md) — `calendar-core` provides the foundational date primitives for `@suaveplan/datetime`: coercing heterogeneous `DateInput`…
|
||||
- [calendar-duration](./src/calendar/calendar-duration.md) — `calendar-duration` provides duration arithmetic for `@suaveplan/datetime`: adding and subtracting `Duration` bags from…
|
||||
- [calendar-grid](./src/calendar/calendar-grid.md) — `calendar-grid` provides `getCalendarMonth`, which builds a full calendar month grid for use in UI calendar components…
|
||||
- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime-error](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime-otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_- [recurrence-generate](./src/recurrence/recurrence-generate.md) — `recurrence-generate` provides the internal generator functions that produce infinite sequences of candidate dates for…
|
||||
- [recurrence-helpers](./src/recurrence/recurrence-helpers.md) — `recurrence-helpers` provides the internal constants and low-level date arithmetic utilities shared across the recurren…
|
||||
- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_- [business-days](./src/business-days/business-days.md) — <!-- begin:auto-docs --> _(no submodules)_- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- [calendar-boundary](./src/calendar/calendar-boundary.md) — `calendar-boundary` provides `startOf` and `endOf` functions for `@suaveplan/datetime`, computing the precise first and…
|
||||
- [calendar-core](./src/calendar/calendar-core.md) — `calendar-core` provides the foundational date primitives for `@suaveplan/datetime`: coercing heterogeneous `DateInput`…
|
||||
- [calendar-duration](./src/calendar/calendar-duration.md) — `calendar-duration` provides duration arithmetic for `@suaveplan/datetime`: adding and subtracting `Duration` bags from…
|
||||
- [calendar-grid](./src/calendar/calendar-grid.md) — `calendar-grid` provides `getCalendarMonth`, which builds a full calendar month grid for use in UI calendar components…
|
||||
- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime-error](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [datetime-otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_- [recurrence-generate](./src/recurrence/recurrence-generate.md) — `recurrence-generate` provides the internal generator functions that produce infinite sequences of candidate dates for…
|
||||
- [recurrence-helpers](./src/recurrence/recurrence-helpers.md) — `recurrence-helpers` provides the internal constants and low-level date arithmetic utilities shared across the recurren…
|
||||
- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_- [business-days](./src/business-days/business-days.md) — <!-- begin:auto-docs --> _(no submodules)_- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_ - [calendar-boundary](./src/calendar/calendar-boundary.md) — `calendar-boundary` provides `startOf` and `endOf` functions for `@suaveplan/datetime`, computing the precise first and…
|
||||
- [calendar-core](./src/calendar/calendar-core.md) — `calendar-core` provides the foundational date primitives for `@suaveplan/datetime`: coercing heterogeneous `DateInput`…
|
||||
- [calendar-duration](./src/calendar/calendar-duration.md) — `calendar-duration` provides duration arithmetic for `@suaveplan/datetime`: adding and subtracting `Duration` bags from…
|
||||
- [calendar-grid](./src/calendar/calendar-grid.md) — `calendar-grid` provides `getCalendarMonth`, which builds a full calendar month grid for use in UI calendar components…
|
||||
- [calendar](./src/calendar/calendar.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [datetime-error](./src/errors/datetime-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [datetime-otel](./src/otel/datetime-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_ - [recurrence-generate](./src/recurrence/recurrence-generate.md) — `recurrence-generate` provides the internal generator functions that produce infinite sequences of candidate dates for…
|
||||
- [recurrence-helpers](./src/recurrence/recurrence-helpers.md) — `recurrence-helpers` provides the internal constants and low-level date arithmetic utilities shared across the recurren…
|
||||
- [recurrence](./src/recurrence/recurrence.md) — <!-- begin:auto-docs --> _(no submodules)_- [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_ - [relative](./src/relative/relative.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|---|---|
|
||||
| [`calendar`](src/calendar/calendar.md) | Core date arithmetic: `toDate`, `addDuration`, `subtractDuration`, `startOf`, `endOf`, `getCalendarMonth`, `daysInMonth`, `isLeapYear`, `isSameDay`, `isSameMonth` |
|
||||
| [`business-days`](src/business-days/business-days.md) | Working-day arithmetic with holiday and custom-weekend support |
|
||||
| [`recurrence`](src/recurrence/recurrence.md) | RRULE parse/format/expand engine (DAILY/WEEKLY/MONTHLY/YEARLY) |
|
||||
| [`relative`](src/relative/relative.md) | Human-readable relative time: `formatRelative`, `timeAgo`, `timeUntil`, `diffInUnits` |
|
||||
| [`errors`](src/errors/datetime-error.md) | `DatetimeError` + `DATETIME_CODES` registry |
|
||||
| [`otel`](src/otel/datetime-otel.md) | OpenTelemetry histogram and counter instrumentation |
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { addDuration, getCalendarMonth, businessDaysBetween, expandRecurrence, parseRRule, formatRelative } from "@suaveplan/datetime";
|
||||
|
||||
// Calendar math
|
||||
const nextMonth = addDuration(new Date(), { months: 1 });
|
||||
const march2024 = getCalendarMonth(2024, 3);
|
||||
|
||||
// Business days
|
||||
const tradingDays = businessDaysBetween("2024-01-01", "2024-04-01");
|
||||
|
||||
// Recurrence
|
||||
const meetings = expandRecurrence(parseRRule("FREQ=WEEKLY;BYDAY=MO,WE;COUNT=8"));
|
||||
|
||||
// Relative time
|
||||
console.log(formatRelative(new Date(Date.now() - 3 * 3600_000))); // "3 hours ago"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/datetime
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# business-days
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `business-days` module provides a complete suite of working-day arithmetic functions for `@suaveplan/datetime`. It models "business day" as any day that is neither in the caller-supplied `weekendDays` set (default: Sunday and Saturday) nor in the caller-supplied `holidays` list. This allows the same API to serve different jurisdictions and corporate calendars without baking in any cultural assumptions.
|
||||
|
||||
All functions accept the polymorphic `DateInput` type and delegate to `toDate` for coercion, so callers can pass ISO strings, epoch milliseconds, or `Date` objects interchangeably.
|
||||
|
||||
## Features
|
||||
|
||||
- `isBusinessDay` — predicate for whether a given date is a working day
|
||||
- `nextBusinessDay` / `previousBusinessDay` — scan forward/backward for the nearest working day
|
||||
- `addBusinessDays` / `subtractBusinessDays` — advance or retreat by an exact count of working days
|
||||
- `businessDaysBetween` — count working days in a half-open interval `[start, end)`
|
||||
- Configurable `weekendDays` (any subset of 0–6)
|
||||
- Holiday exclusion via an `Date[]` array
|
||||
- Cooperative cancellation via `AbortSignal` (checked every 256 iterations in `businessDaysBetween`)
|
||||
- OTel counter via `recordBusinessDaysBetween`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { isBusinessDay, addBusinessDays } from "@suaveplan/datetime/business-days";
|
||||
|
||||
// Simple weekday check
|
||||
console.log(isBusinessDay("2024-03-18")); // true (Monday)
|
||||
console.log(isBusinessDay("2024-03-16")); // false (Saturday)
|
||||
|
||||
// Advance 5 business days
|
||||
const result = addBusinessDays("2024-03-18", 5);
|
||||
console.log(result.toDateString()); // Fri Mar 29 2024 (skips weekend)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
businessDaysBetween,
|
||||
nextBusinessDay,
|
||||
} from "@suaveplan/datetime/business-days";
|
||||
|
||||
const nyseHolidays = [
|
||||
new Date("2024-01-01"), // New Year's Day
|
||||
new Date("2024-07-04"), // Independence Day
|
||||
];
|
||||
|
||||
// How many NYSE trading days in Q1 2024?
|
||||
const q1 = businessDaysBetween("2024-01-01", "2024-04-01", {
|
||||
holidays: nyseHolidays,
|
||||
});
|
||||
console.log(q1); // ~62 trading days
|
||||
|
||||
// Cooperative cancellation
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 10);
|
||||
const days = businessDaysBetween("2020-01-01", "2030-01-01", {
|
||||
signal: controller.signal,
|
||||
});
|
||||
console.log(days); // partial count, aborted after ~256 iterations
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `isBusinessDay(date: DateInput, opts?: BusinessDayOptions): boolean`
|
||||
|
||||
Returns `true` when `date` is not a weekend day and not a holiday.
|
||||
|
||||
- `opts.weekendDays` — default `[0, 6]` (Sun, Sat)
|
||||
- `opts.holidays` — list of `Date` objects to exclude
|
||||
|
||||
### `nextBusinessDay(date: DateInput, opts?: BusinessDayOptions): Date`
|
||||
|
||||
Returns the first business day strictly after `date`.
|
||||
|
||||
### `previousBusinessDay(date: DateInput, opts?: BusinessDayOptions): Date`
|
||||
|
||||
Returns the first business day strictly before `date`.
|
||||
|
||||
### `addBusinessDays(date: DateInput, n: number, opts?: BusinessDayOptions): Date`
|
||||
|
||||
Advance `date` by exactly `|n|` business days. Negative `n` moves backward.
|
||||
|
||||
### `subtractBusinessDays(date: DateInput, n: number, opts?: BusinessDayOptions): Date`
|
||||
|
||||
Move `date` backward by exactly `n` business days. Delegates to `addBusinessDays(date, -n)`.
|
||||
|
||||
### `businessDaysBetween(start: DateInput, end: DateInput, opts?: BusinessDayOptions): number`
|
||||
|
||||
Count business days in `[start, end)`. Returns a negative number when `end < start`. Checks `opts.signal` every 256 iterations.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Holiday lookup uses a `Set<string>` keyed by `"YYYY-M-D"` (local time) built once per call. This is O(1) per day check and avoids allocating `Date` objects for the comparison.
|
||||
|
||||
All date arithmetic is performed at local-midnight epoch millis to prevent time-of-day accumulation errors across DST transitions. The `midnight()` helper normalises any input to 00:00:00.000 local time before iteration begins.
|
||||
|
||||
`addBusinessDays(d, 0)` returns the local midnight of `d` regardless of whether `d` itself is a business day, because zero steps means no movement.
|
||||
@@ -0,0 +1,99 @@
|
||||
# calendar-boundary
|
||||
|
||||
## Purpose
|
||||
|
||||
`calendar-boundary` provides `startOf` and `endOf` functions for `@suaveplan/datetime`, computing the precise first and last moments of any calendar unit (year, month, week, day, hour, minute, second, or millisecond) for a given date. These are the building blocks for date-range queries, fiscal period calculations, and UI calendar display logic.
|
||||
|
||||
Both functions are pure and stateless. They return new `Date` objects without mutating their inputs and operate in local time.
|
||||
|
||||
## Features
|
||||
|
||||
- `startOf` — first moment of the given unit (midnight for day, 00:00:00.000 for hours, etc.)
|
||||
- `endOf` — last millisecond of the given unit (23:59:59.999 for day)
|
||||
- Supports all `DurationUnit` values: `years`, `months`, `weeks`, `days`, `hours`, `minutes`, `seconds`, `milliseconds`
|
||||
- `weeks` uses Sunday as the week start (matching `Date.prototype.getDay()`)
|
||||
- `BoundaryOptions` interface with optional `clock` for future extension
|
||||
- `milliseconds` unit returns the date unchanged (identity)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { startOf, endOf } from "@suaveplan/datetime";
|
||||
|
||||
const d = new Date(2024, 2, 15, 14, 30, 45, 500); // Mar 15 2024 14:30:45.500
|
||||
|
||||
const dayStart = startOf(d, "days");
|
||||
// 2024-03-15 00:00:00.000
|
||||
|
||||
const monthEnd = endOf(d, "months");
|
||||
// 2024-03-31 23:59:59.999
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { startOf, endOf } from "@suaveplan/datetime";
|
||||
|
||||
// Fiscal year boundaries
|
||||
const q1Start = startOf("2024-03-15", "years"); // Jan 1 2024
|
||||
const q1End = endOf("2024-03-31", "months"); // Mar 31 2024 23:59:59.999
|
||||
|
||||
// Current week range (Sunday–Saturday)
|
||||
const today = new Date();
|
||||
const weekStart = startOf(today, "weeks");
|
||||
const weekEnd = endOf(today, "weeks");
|
||||
|
||||
// Hour slot boundaries
|
||||
const slotStart = startOf(new Date(2024, 0, 1, 9, 45, 0), "hours");
|
||||
// 2024-01-01 09:00:00.000
|
||||
const slotEnd = endOf(new Date(2024, 0, 1, 9, 45, 0), "hours");
|
||||
// 2024-01-01 09:59:59.999
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `BoundaryOptions`
|
||||
|
||||
```ts
|
||||
interface BoundaryOptions {
|
||||
clock?: Clock;
|
||||
}
|
||||
```
|
||||
|
||||
Reserved for future use (e.g. timezone-aware boundaries). Currently unused in the function bodies.
|
||||
|
||||
### `startOf(date: DateInput, unit: DurationUnit, opts?: BoundaryOptions): Date`
|
||||
|
||||
Returns the first moment of the `unit` period containing `date`.
|
||||
|
||||
| Unit | Result |
|
||||
|------|--------|
|
||||
| `years` | Jan 1 00:00:00.000 |
|
||||
| `months` | 1st of month 00:00:00.000 |
|
||||
| `weeks` | Sunday of the week 00:00:00.000 |
|
||||
| `days` | midnight 00:00:00.000 |
|
||||
| `hours` | :00:00.000 of the hour |
|
||||
| `minutes` | :00.000 of the minute |
|
||||
| `seconds` | .000 of the second |
|
||||
| `milliseconds` | unchanged |
|
||||
|
||||
### `endOf(date: DateInput, unit: DurationUnit, opts?: BoundaryOptions): Date`
|
||||
|
||||
Returns the last millisecond of the `unit` period containing `date`.
|
||||
|
||||
| Unit | Result |
|
||||
|------|--------|
|
||||
| `years` | Dec 31 23:59:59.999 |
|
||||
| `months` | Last day 23:59:59.999 |
|
||||
| `weeks` | Saturday of the week 23:59:59.999 |
|
||||
| `days` | 23:59:59.999 |
|
||||
| `hours` | :59:59.999 |
|
||||
| `minutes` | :59.999 |
|
||||
| `seconds` | .999 |
|
||||
| `milliseconds` | unchanged |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`startOf("weeks")` subtracts `date.getDay()` days (0 = Sunday) to find the Sunday anchor. `endOf("weeks")` adds `6 - date.getDay()` to reach Saturday. This matches the JavaScript `Date.getDay()` convention of Sunday = 0. Callers needing Monday-start weeks should offset by one day using `addDuration`.
|
||||
|
||||
`endOf("months")` calls `daysInMonth` to find the last valid day of the month, correctly handling February on leap years.
|
||||
@@ -0,0 +1,82 @@
|
||||
# calendar-core
|
||||
|
||||
## Purpose
|
||||
|
||||
`calendar-core` provides the foundational date primitives for `@suaveplan/datetime`: coercing heterogeneous `DateInput` values into `Date` objects, predicates for same-day and same-month equality, leap year detection, and days-in-month calculation. These are the lowest-level building blocks consumed by every other calendar sub-module.
|
||||
|
||||
All functions are pure and stateless. They accept local-time values and return local-time `Date` objects. There are no side effects and no external dependencies beyond `@suaveplan/error`.
|
||||
|
||||
## Features
|
||||
|
||||
- `toDate` — coerce `Date | string | number` to `Date`, throws `DatetimeError` for invalid inputs
|
||||
- `asTimezone` — brands a raw string as the opaque `Timezone` type
|
||||
- `isLeapYear` — true for proleptic Gregorian leap years
|
||||
- `daysInMonth` — correct day-count for any month, including February on leap years
|
||||
- `isSameDay` — calendar equality predicate (year + month + date)
|
||||
- `isSameMonth` — calendar month equality predicate (year + month)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { toDate, isLeapYear, daysInMonth, isSameDay } from "@suaveplan/datetime";
|
||||
|
||||
const d = toDate("2024-02-15");
|
||||
console.log(isLeapYear(2024)); // true
|
||||
console.log(daysInMonth(2024, 2)); // 29
|
||||
console.log(isSameDay(d, new Date(2024, 1, 15))); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { toDate, isSameMonth, asTimezone } from "@suaveplan/datetime";
|
||||
|
||||
const jan1 = toDate(new Date(2024, 0, 1));
|
||||
const jan31 = toDate("2024-01-31");
|
||||
const feb1 = toDate(1706745600000); // epoch ms for 2024-02-01
|
||||
|
||||
console.log(isSameMonth(jan1, jan31)); // true
|
||||
console.log(isSameMonth(jan31, feb1)); // false
|
||||
|
||||
// Error on invalid input
|
||||
try {
|
||||
toDate("not-a-date");
|
||||
} catch (e) {
|
||||
console.log((e as { code: string }).code); // DATETIME.PARSE.INVALID_INPUT
|
||||
}
|
||||
|
||||
const tz = asTimezone("America/New_York");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `toDate(input: DateInput): Date`
|
||||
|
||||
Coerce `input` to a `Date`. Accepts `Date`, ISO string, or epoch millis.
|
||||
Throws `DatetimeError("DATETIME.PARSE.INVALID_INPUT")` for unparseable strings or `NaN` inputs.
|
||||
|
||||
### `asTimezone(tz: string): Timezone`
|
||||
|
||||
Cast `tz` to the branded `Timezone` type. No runtime validation is performed.
|
||||
|
||||
### `isLeapYear(year: number): boolean`
|
||||
|
||||
Returns `true` for proleptic Gregorian leap years (divisible by 4, not 100, or divisible by 400).
|
||||
|
||||
### `daysInMonth(year: number, month: number): number`
|
||||
|
||||
Returns the number of days in the given month (1-indexed). Handles February leap-year logic via `new Date(year, month, 0).getDate()`.
|
||||
|
||||
### `isSameDay(a: DateInput, b: DateInput): boolean`
|
||||
|
||||
Returns `true` when `a` and `b` share the same calendar year, month, and date.
|
||||
|
||||
### `isSameMonth(a: DateInput, b: DateInput): boolean`
|
||||
|
||||
Returns `true` when `a` and `b` share the same calendar year and month.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`daysInMonth` uses the "day 0 of the following month" trick: `new Date(year, month, 0).getDate()` returns the last day of the prior month, which equals the number of days in that month. This avoids a lookup table and handles leap years correctly via the JavaScript `Date` constructor.
|
||||
|
||||
`toDate` copies `Date` inputs (returns `new Date(input.getTime())`) to prevent mutation aliasing. String inputs are passed directly to `new Date()` which follows ISO 8601 parsing rules in all modern runtimes.
|
||||
@@ -0,0 +1,73 @@
|
||||
# calendar-duration
|
||||
|
||||
## Purpose
|
||||
|
||||
`calendar-duration` provides duration arithmetic for `@suaveplan/datetime`: adding and subtracting `Duration` bags from dates with correct month-end clamping and overflow normalisation. It is the arithmetic engine that powers use cases such as "one month from January 31" and "subtract 90 days".
|
||||
|
||||
The module separates year/month arithmetic (done in calendar coordinates to avoid day-count ambiguity) from sub-day arithmetic (done as millisecond offsets on a local-midnight base date).
|
||||
|
||||
## Features
|
||||
|
||||
- `addDuration` — add a `Duration` bag to a date with month-end clamping
|
||||
- `subtractDuration` — subtract a `Duration` bag (negates fields, delegates to `addDuration`)
|
||||
- Handles all `DurationUnit` fields: `years`, `months`, `weeks`, `days`, `hours`, `minutes`, `seconds`, `milliseconds`
|
||||
- Month overflow/underflow normalisation (e.g. month 13 → January of next year)
|
||||
- Day clamping prevents invalid dates (e.g. Jan 31 + 1 month = Feb 28/29)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { addDuration, subtractDuration } from "@suaveplan/datetime";
|
||||
|
||||
const jan31 = new Date(2024, 0, 31);
|
||||
const feb29 = addDuration(jan31, { months: 1 });
|
||||
console.log(feb29.toDateString()); // Thu Feb 29 2024 (leap year)
|
||||
|
||||
const yesterday = subtractDuration(new Date(), { days: 1 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { addDuration } from "@suaveplan/datetime";
|
||||
|
||||
// Combined duration: 1 year, 2 months, 3 days, 4 hours
|
||||
const base = new Date(2023, 10, 30, 12, 0, 0, 0); // Nov 30 2023 12:00
|
||||
const result = addDuration(base, {
|
||||
years: 1,
|
||||
months: 2,
|
||||
days: 3,
|
||||
hours: 4,
|
||||
});
|
||||
// Feb 2, 2025 16:00 — Nov 30 + 1y2m = Jan 30 2025, then +3d = Feb 2, +4h = 16:00
|
||||
|
||||
// Month underflow normalisation: subtracting months past January
|
||||
const dec1 = new Date(2024, 11, 1);
|
||||
const oct = addDuration(dec1, { months: -2 });
|
||||
console.log(oct.getMonth()); // 9 (October)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `addDuration(date: DateInput, duration: Duration): Date`
|
||||
|
||||
Add `duration` to `date`. Year/month fields are applied first with overflow/underflow normalisation. Day/week/hour/minute/second/millisecond fields are added as a millisecond offset to the clamped local-midnight date.
|
||||
|
||||
**Params:**
|
||||
|
||||
- `date` — `Date`, ISO string, or epoch number
|
||||
- `duration` — `Duration` bag; all fields are optional and default to `0`
|
||||
|
||||
**Returns:** New `Date` (does not mutate input).
|
||||
|
||||
### `subtractDuration(date: DateInput, duration: Duration): Date`
|
||||
|
||||
Subtract `duration` from `date`. Internally negates all fields and delegates to `addDuration`.
|
||||
|
||||
**Params:** same as `addDuration`.
|
||||
|
||||
**Returns:** New `Date`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Year and month arithmetic is performed in integer calendar coordinates rather than millisecond arithmetic to avoid the 28–31 day ambiguity. After clamping the day to the last valid day of the resulting month, the remaining duration components (weeks, days, hours, minutes, seconds, milliseconds) are totalled as a single millisecond value and added to a local-midnight `Date` object. This approach avoids DST-induced errors that would occur if each component were added sequentially in timestamp space.
|
||||
@@ -0,0 +1,78 @@
|
||||
# calendar-grid
|
||||
|
||||
## Purpose
|
||||
|
||||
`calendar-grid` provides `getCalendarMonth`, which builds a full calendar month grid for use in UI calendar components and scheduling displays. It returns a `CalendarMonth` value containing one `CalendarDay` entry per day in the month, each annotated with its day-of-week index, whether it is a weekend, and whether it represents today (driven by an injectable `Clock`).
|
||||
|
||||
The module is intentionally narrow: it answers "what are the days in this month and which one is today?" — layout concerns (week rows, leading/trailing days from adjacent months) are left to the consumer.
|
||||
|
||||
## Features
|
||||
|
||||
- `getCalendarMonth` — builds a `CalendarMonth` grid for a given year and 1-indexed month
|
||||
- Each `CalendarDay` includes `date`, `dayOfWeek` (0=Sun … 6=Sat), `isWeekend`, and `isToday`
|
||||
- Injectable `Clock` for deterministic rendering in tests and server-side snapshots
|
||||
- Zero side effects; all computation is local
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { getCalendarMonth } from "@suaveplan/datetime";
|
||||
|
||||
const march = getCalendarMonth(2024, 3);
|
||||
console.log(march.days.length); // 31
|
||||
console.log(march.days[0]?.dayOfWeek); // 5 (Friday — Mar 1 2024)
|
||||
const weekends = march.days.filter((d) => d.isWeekend);
|
||||
console.log(weekends.length); // 10
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { getCalendarMonth } from "@suaveplan/datetime";
|
||||
|
||||
// Deterministic "today" for SSR or snapshot tests
|
||||
const fakeClock = () => new Date(2024, 2, 15).getTime(); // Mar 15 2024
|
||||
const march = getCalendarMonth(2024, 3, { clock: fakeClock });
|
||||
|
||||
const today = march.days.find((d) => d.isToday);
|
||||
console.log(today?.date.getDate()); // 15
|
||||
|
||||
// Render a simple ASCII calendar header
|
||||
for (const day of march.days) {
|
||||
const marker = day.isToday ? "*" : day.isWeekend ? "W" : " ";
|
||||
process.stdout.write(`${marker}${String(day.date.getDate()).padStart(2)} `);
|
||||
if (day.dayOfWeek === 6) process.stdout.write("\n");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CalendarMonthOptions`
|
||||
|
||||
```ts
|
||||
interface CalendarMonthOptions {
|
||||
clock?: Clock;
|
||||
}
|
||||
```
|
||||
|
||||
- `clock` — injectable `Clock` (returns epoch ms). Defaults to `Date.now`.
|
||||
|
||||
### `getCalendarMonth(year: number, month: number, opts?: CalendarMonthOptions): CalendarMonth`
|
||||
|
||||
Build a `CalendarMonth` for the given `year` and 1-indexed `month`.
|
||||
|
||||
**Params:**
|
||||
|
||||
- `year` — full calendar year (e.g. 2024)
|
||||
- `month` — 1-indexed month (1 = January, 12 = December)
|
||||
- `opts.clock` — optional injectable clock
|
||||
|
||||
**Returns:** `{ year, month, days: CalendarDay[] }` where each `CalendarDay` is:
|
||||
|
||||
```ts
|
||||
{ date: Date; dayOfWeek: Weekday; isWeekend: boolean; isToday: boolean }
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`isToday` is computed by comparing a `"year-month-date"` string derived from `clock()` to the same string for each grid day. This string comparison avoids UTC/local timezone confusion that would arise from comparing epoch timestamps directly. The `clock` default (`Date.now`) is captured once per call to ensure consistent "today" across all days in the grid.
|
||||
@@ -0,0 +1,112 @@
|
||||
# calendar
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `calendar` module is the arithmetic core of `@suaveplan/datetime`. It provides the fundamental building blocks for working with dates: coercing heterogeneous `DateInput` values into `Date` objects, adding and subtracting `Duration` bags with correct month-end clamping, computing start-of / end-of boundaries for any time unit, and generating full calendar-month grids with today-awareness injected via a pluggable `Clock`.
|
||||
|
||||
All functions are pure and stateless. They accept local-time `Date` objects and return local-time `Date` objects. Timezone conversion (IANA) is not in scope for this module — it belongs to consumer code that formats dates for display.
|
||||
|
||||
## Features
|
||||
|
||||
- `toDate` — coerce `Date | string | number` to `Date`, throws `DatetimeError` for invalid inputs
|
||||
- `isLeapYear` — true for proleptic Gregorian leap years
|
||||
- `daysInMonth` — correct day-count for any month including February on leap years
|
||||
- `isSameDay` / `isSameMonth` — calendar equality predicates
|
||||
- `addDuration` / `subtractDuration` — month-clamping arithmetic (Jan 31 + 1 month = Feb 28/29)
|
||||
- `startOf` / `endOf` — precise boundary computation for years, months, weeks, days, hours, minutes, seconds, milliseconds
|
||||
- `getCalendarMonth` — full grid of `CalendarDay` entries with `isToday` driven by injected `Clock`
|
||||
- `asTimezone` — brands a raw string as a `Timezone` opaque type
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { toDate, addDuration, isSameDay } from "@suaveplan/datetime/calendar";
|
||||
|
||||
const jan31 = toDate("2024-01-31");
|
||||
const feb29 = addDuration(jan31, { months: 1 });
|
||||
console.log(feb29.toISOString()); // 2024-02-29T00:00:00.000Z (leap year)
|
||||
|
||||
console.log(isSameDay(feb29, toDate("2024-02-29"))); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
getCalendarMonth,
|
||||
startOf,
|
||||
endOf,
|
||||
subtractDuration,
|
||||
} from "@suaveplan/datetime/calendar";
|
||||
|
||||
// Inject a fake clock for deterministic "isToday" rendering in tests
|
||||
const fakeClock = () => new Date("2024-03-15T12:00:00Z").getTime();
|
||||
|
||||
const march = getCalendarMonth(2024, 3, { clock: fakeClock });
|
||||
const today = march.days.find((d) => d.isToday);
|
||||
console.log(today?.date.getDate()); // 15
|
||||
|
||||
// Fiscal quarter boundaries
|
||||
const q1Start = startOf("2024-03-15", "years"); // 2024-01-01
|
||||
const q1End = endOf("2024-03-31", "months"); // 2024-03-31T23:59:59.999
|
||||
|
||||
// Rolling 90-day window
|
||||
const ninetyDaysAgo = subtractDuration(new Date(), { days: 90 });
|
||||
console.log(ninetyDaysAgo.toISOString());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `toDate(input: DateInput): Date`
|
||||
|
||||
Coerce `input` to a `Date`. Accepts `Date`, ISO string, or epoch millis.
|
||||
Throws `DatetimeError("DATETIME.PARSE.INVALID_INPUT")` for unparseable strings or `NaN` inputs.
|
||||
|
||||
### `asTimezone(tz: string): Timezone`
|
||||
|
||||
Cast `tz` to the branded `Timezone` type. No runtime validation.
|
||||
|
||||
### `isLeapYear(year: number): boolean`
|
||||
|
||||
Returns `true` for proleptic Gregorian leap years.
|
||||
|
||||
### `daysInMonth(year: number, month: number): number`
|
||||
|
||||
Returns the number of days in the given month (1-indexed). Handles February leap-year logic.
|
||||
|
||||
### `isSameDay(a: DateInput, b: DateInput): boolean`
|
||||
|
||||
Returns `true` when `a` and `b` share the same calendar year, month, and day.
|
||||
|
||||
### `isSameMonth(a: DateInput, b: DateInput): boolean`
|
||||
|
||||
Returns `true` when `a` and `b` share the same calendar year and month.
|
||||
|
||||
### `addDuration(date: DateInput, duration: Duration): Date`
|
||||
|
||||
Add `duration` to `date`. Year/month fields are applied first with overflow normalisation. Day/week/hour/minute/second/millisecond fields are added as millisecond offsets. Month-end clamping prevents invalid dates (e.g. March 31 - 1 month = Feb 28/29).
|
||||
|
||||
### `subtractDuration(date: DateInput, duration: Duration): Date`
|
||||
|
||||
Subtract `duration` from `date`. Internally negates all fields and delegates to `addDuration`.
|
||||
|
||||
### `startOf(date: DateInput, unit: DurationUnit, opts?: BoundaryOptions): Date`
|
||||
|
||||
Returns the first moment of the given `unit` period. For `weeks`, Sunday is the start of the week.
|
||||
|
||||
### `endOf(date: DateInput, unit: DurationUnit, opts?: BoundaryOptions): Date`
|
||||
|
||||
Returns the last millisecond of the given `unit` period.
|
||||
|
||||
### `getCalendarMonth(year: number, month: number, opts?: CalendarMonthOptions): CalendarMonth`
|
||||
|
||||
Build a `CalendarMonth` grid. `opts.clock` defaults to `Date.now`. Each `CalendarDay` includes `date`, `dayOfWeek`, `isWeekend`, and `isToday`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Month arithmetic is performed in calendar coordinates (year/month/day integers) rather than millisecond arithmetic to avoid the 28–31 day ambiguity. Day clamping uses `daysInMonth` to find the last valid day after year/month adjustment, then the remaining sub-day duration components are added as a single millisecond offset to the clamped local-midnight date.
|
||||
|
||||
`startOf("weeks")` uses Sunday as the week start (index 0) to match `Date.prototype.getDay()`. Callers that need Monday-start weeks should offset the result by one day.
|
||||
@@ -0,0 +1,91 @@
|
||||
# datetime-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`DatetimeError` is the typed error class for the entire `@suaveplan/datetime` package. It extends the project-wide `SuaveplanError` base class with a narrow `DatetimeCode` type parameter, allowing callers to discriminate error origins with `instanceof` and narrow the `code` property to one of three categories: `PARSE.INVALID_INPUT`, `RECURRENCE.INFINITE_EXPANSION`, and `TIMEZONE.UNKNOWN`.
|
||||
|
||||
Keeping all error definitions in a single, versioned registry (`DATETIME_CODES`) means consumers can exhaustively handle failure modes without consulting documentation or source code.
|
||||
|
||||
## Features
|
||||
|
||||
- Inherits full `SuaveplanError` surface: `code`, `context`, `timestamp`, `traceId`, `cause`
|
||||
- Three error codes covering the library's failure surface
|
||||
- `defineCodes` ensures the registry is frozen and type-inferred at build time
|
||||
- Compatible with `walkCauses`, `toSpanEvent`, and the `@suaveplan/error` wire format
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { DatetimeError, DATETIME_CODES } from "@suaveplan/datetime/errors";
|
||||
|
||||
throw new DatetimeError(
|
||||
DATETIME_CODES["PARSE.INVALID_INPUT"],
|
||||
"datetime.parse.invalid_input",
|
||||
{ context: { input: "not-a-date" } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { DatetimeError, DATETIME_CODES } from "@suaveplan/datetime/errors";
|
||||
|
||||
function guardExpansion(hasTerminator: boolean): void {
|
||||
if (!hasTerminator) {
|
||||
throw new DatetimeError(
|
||||
DATETIME_CODES["RECURRENCE.INFINITE_EXPANSION"],
|
||||
"datetime.recurrence.infinite_expansion",
|
||||
{
|
||||
context: { hint: "supply count or until" },
|
||||
traceId: "abc-123",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
guardExpansion(false);
|
||||
} catch (err) {
|
||||
if (err instanceof DatetimeError) {
|
||||
// code is narrowed to DatetimeCode
|
||||
console.error(err.code, err.context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `DATETIME_CODES`
|
||||
|
||||
`Record<"PARSE.INVALID_INPUT" | "RECURRENCE.INFINITE_EXPANSION" | "TIMEZONE.UNKNOWN", string>`
|
||||
|
||||
Frozen registry of all datetime error codes. Values are fully-qualified strings of the form `DATETIME.<reason>`.
|
||||
|
||||
### `DatetimeCode`
|
||||
|
||||
`type DatetimeCode = (typeof DATETIME_CODES)[keyof typeof DATETIME_CODES]`
|
||||
|
||||
Union of all valid datetime error code strings.
|
||||
|
||||
### `class DatetimeError extends SuaveplanError<DatetimeCode>`
|
||||
|
||||
**Constructor:** `new DatetimeError(code: DatetimeCode, messageKey: string, options?: SuaveplanErrorOptions)`
|
||||
|
||||
- `code` — one of the values in `DATETIME_CODES`
|
||||
- `messageKey` — i18n key passed to `resolveMessage`
|
||||
- `options` — optional `{ cause, context, clock, traceId }`
|
||||
|
||||
**Inherited properties:** `code`, `context`, `timestamp`, `traceId`, `cause`, `name`, `stack`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The three-code surface was chosen to be exhaustive yet minimal:
|
||||
|
||||
- Parsing errors cover any `DateInput` coercion failure
|
||||
- Recurrence expansion errors guard against infinite loops
|
||||
- Timezone errors are reserved for future IANA name validation
|
||||
|
||||
The `defineCodes` helper freezes the registry object, preventing accidental mutation and enabling tree-shaking of unused codes.
|
||||
@@ -0,0 +1,69 @@
|
||||
# datetime-otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`datetime-otel` provides lightweight OpenTelemetry instrumentation for the `@suaveplan/datetime` package. It registers a histogram for `expandRecurrence` call durations and a counter for `businessDaysBetween` interval evaluations. Both instruments are initialised lazily via `metrics.getMeter()`, which is a no-op when no global `MeterProvider` is configured, ensuring zero overhead in environments without observability infrastructure.
|
||||
|
||||
The module exposes thin wrapper functions (`recordExpandRecurrence`, `recordBusinessDaysBetween`) so the hot-path modules do not need to import `@opentelemetry/api` directly.
|
||||
|
||||
## Features
|
||||
|
||||
- `datetime.expand_recurrence.duration_ms` histogram (unit: `ms`)
|
||||
- `datetime.business_days_between.total` counter (unit: `{days}`)
|
||||
- Zero-overhead when no OTel SDK is configured
|
||||
- Named meter `@suaveplan/datetime` for easy filtering in dashboards
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { recordExpandRecurrence } from "@suaveplan/datetime/otel";
|
||||
|
||||
const start = Date.now();
|
||||
// ... run expandRecurrence ...
|
||||
recordExpandRecurrence(Date.now() - start);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { recordBusinessDaysBetween } from "@suaveplan/datetime/otel";
|
||||
|
||||
// Wire up an SDK meter provider before the first import of datetime modules
|
||||
const provider = new MeterProvider();
|
||||
// metrics.setGlobalMeterProvider(provider); // done at app startup
|
||||
|
||||
// Later, inside businessDaysBetween implementation:
|
||||
recordBusinessDaysBetween(365); // records 365 day-intervals evaluated
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `expandRecurrenceDuration`
|
||||
|
||||
`Histogram` — OpenTelemetry histogram for `expandRecurrence` durations.
|
||||
|
||||
### `businessDaysBetweenTotal`
|
||||
|
||||
`Counter` — OpenTelemetry counter for `businessDaysBetween` day-intervals.
|
||||
|
||||
### `recordExpandRecurrence(durationMs: number): void`
|
||||
|
||||
Records a single `expandRecurrence` call duration. Call immediately after the expansion completes.
|
||||
|
||||
- `durationMs` — wall-clock elapsed time in milliseconds
|
||||
|
||||
### `recordBusinessDaysBetween(count: number): void`
|
||||
|
||||
Increments the `businessDaysBetween` counter by the number of calendar days evaluated.
|
||||
|
||||
- `count` — number of calendar-day intervals walked during the call
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`metrics.getMeter()` is called once at module initialisation. The returned `Meter` instance is cached for the lifetime of the process. In environments without a registered `MeterProvider`, all record/add calls are no-ops at the OTel SDK level, incurring only a function-call overhead.
|
||||
|
||||
The histogram uses millisecond granularity, which is sufficient for typical recurrence expansions (tens to hundreds of milliseconds). For sub-millisecond benchmarks, use the `mitata` benchmark suite instead.
|
||||
@@ -0,0 +1,95 @@
|
||||
# recurrence-generate
|
||||
|
||||
## Purpose
|
||||
|
||||
`recurrence-generate` provides the internal generator functions that produce infinite sequences of candidate dates for each recurrence frequency supported by `@suaveplan/datetime`. Each generator yields dates strictly after a given `from` anchor, and the `generateCandidates` dispatcher routes to the correct generator based on `RecurrenceRule.freq`.
|
||||
|
||||
This module is an internal implementation detail. Public callers should use `expandRecurrence` and `nextOccurrence` from `recurrence.ts`.
|
||||
|
||||
## Features
|
||||
|
||||
- `generateDaily` — daily step with optional `BYDAY` weekday filter
|
||||
- `generateWeekly` — weekly step, either plain (same weekday) or with `BYDAY` multi-day selection
|
||||
- `generateMonthly` — monthly step supporting `BYMONTHDAY`, `BYDAY`, or plain monthly recurrence
|
||||
- `generateYearly` — yearly step supporting `BYMONTHDAY`, `BYDAY`, or plain yearly recurrence
|
||||
- `generateCandidates` — dispatcher that routes to the correct generator from `RecurrenceRule.freq`
|
||||
- All generators are infinite; termination is enforced by callers (`count` / `until` / `AbortSignal`)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateCandidates } from "./recurrence-generate.js";
|
||||
|
||||
const rule = { freq: "daily" as const, count: 5 };
|
||||
const from = new Date(2024, 0, 1);
|
||||
|
||||
const gen = generateCandidates(rule, from);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
console.log(gen.next().value?.toDateString());
|
||||
}
|
||||
// Tue Jan 02 2024 … Sat Jan 06 2024
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { generateCandidates } from "./recurrence-generate.js";
|
||||
|
||||
// Monthly with BYMONTHDAY — yields the 15th of each month
|
||||
const rule = {
|
||||
freq: "monthly" as const,
|
||||
byMonthDay: [15],
|
||||
};
|
||||
const from = new Date(2024, 0, 1);
|
||||
const gen = generateCandidates(rule, from);
|
||||
|
||||
const first = gen.next().value!;
|
||||
console.log(first.toDateString()); // Mon Jan 15 2024
|
||||
|
||||
const second = gen.next().value!;
|
||||
console.log(second.toDateString()); // Thu Feb 15 2024
|
||||
|
||||
// Weekly with BYDAY — yields Mon/Wed/Fri
|
||||
const weeklyRule = {
|
||||
freq: "weekly" as const,
|
||||
byDay: [1, 3, 5] as const,
|
||||
};
|
||||
const weeklyGen = generateCandidates(weeklyRule, new Date(2024, 2, 11));
|
||||
console.log(weeklyGen.next().value?.getDay()); // 3 (Wed Mar 13)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generateCandidates(rule: RecurrenceRule, from: Date): Generator<Date>`
|
||||
|
||||
Unified dispatcher. Routes to the frequency-specific generator based on `rule.freq`. All yielded dates are strictly after `from`.
|
||||
|
||||
### `generateDaily(rule, from, interval): Generator<Date>`
|
||||
|
||||
Steps `interval` days at a time. If `rule.byDay` is set, only yields dates whose `getDay()` is in the list.
|
||||
|
||||
### `generateWeekly(rule, from, interval): Generator<Date>`
|
||||
|
||||
If `rule.byDay` is set: anchors to the Sunday of the current week, then emits the specified weekdays in sorted order each `interval * 7` days. Without `byDay`: yields the same weekday every `interval * 7` days.
|
||||
|
||||
### `generateMonthly(rule, from, interval): Generator<Date>`
|
||||
|
||||
Three modes:
|
||||
|
||||
1. `byMonthDay` — emits the specified day-of-month values (sorted) in each month, skipping months where a day exceeds `daysInMonth`.
|
||||
2. `byDay` — scans each month and emits all days matching the weekday list.
|
||||
3. Plain — yields the same day-of-month each `interval` months using `addMonths` clamping.
|
||||
|
||||
### `generateYearly(rule, from, interval): Generator<Date>`
|
||||
|
||||
Three modes:
|
||||
|
||||
1. `byMonthDay` — emits specified day-of-month values in each year.
|
||||
2. `byDay` — advances one day at a time from the year anchor, yielding matching weekdays.
|
||||
3. Plain — yields the same date each `interval` years using `addYears`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All generators are implemented as native ES `Generator` functions (`function*`) to enable lazy evaluation. They produce an infinite sequence; the caller (`expandRecurrence`, `nextOccurrence`) is responsible for applying `count`, `until`, and `AbortSignal` termination conditions.
|
||||
|
||||
The `generateWeekly` with `BYDAY` mode anchors to the start of the week (Sunday) on the first iteration, which means it may emit dates in the current week before advancing. Dates before or equal to `from` are filtered out with a `candidate > from` guard.
|
||||
@@ -0,0 +1,98 @@
|
||||
# recurrence-helpers
|
||||
|
||||
## Purpose
|
||||
|
||||
`recurrence-helpers` provides the internal constants and low-level date arithmetic utilities shared across the recurrence sub-modules of `@suaveplan/datetime`. It contains the frequency/day-name mappings used by the RRULE parser and formatter, and the `addDays`, `addMonths`, `addYears`, `daysInMonth`, `toRRuleDate`, and `fromRRuleDate` helpers used by the candidate generators.
|
||||
|
||||
This module is an internal implementation detail. All public API is exposed through `recurrence.ts`. Direct imports are supported but not part of the documented public surface.
|
||||
|
||||
## Features
|
||||
|
||||
- `FREQ_MAP` — maps RRULE `FREQ` strings to `Frequency` values
|
||||
- `FREQ_RRULE` — maps `Frequency` values back to RRULE strings
|
||||
- `DAY_NAME` / `DAY_INDEX` — bidirectional weekday ↔ abbreviation maps
|
||||
- `toRRuleDate` — format a `Date` as `YYYYMMDD`
|
||||
- `fromRRuleDate` — parse `YYYYMMDD` (or `YYYYMMDDTHHmmssZ`) into a `Date`
|
||||
- `daysInMonth` — days in a 1-indexed month (local copy for recurrence module independence)
|
||||
- `addDays` / `addMonths` / `addYears` — safe date arithmetic with month-end clamping
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { toRRuleDate, fromRRuleDate, addDays } from "./recurrence-helpers.js";
|
||||
|
||||
const d = new Date(2024, 0, 31);
|
||||
console.log(toRRuleDate(d)); // "20240131"
|
||||
|
||||
const parsed = fromRRuleDate("20241231");
|
||||
console.log(parsed.getMonth()); // 11 (December)
|
||||
|
||||
const next = addDays(d, 7);
|
||||
console.log(next.getDate()); // 7 (Feb 7 2024)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { addMonths, addYears, daysInMonth } from "./recurrence-helpers.js";
|
||||
|
||||
// Month-end clamping: Jan 31 + 1 month = Feb 29 (leap year 2024)
|
||||
const jan31 = new Date(2024, 0, 31);
|
||||
const feb = addMonths(jan31, 1);
|
||||
console.log(feb.getDate()); // 29
|
||||
|
||||
// Year addition uses addMonths internally
|
||||
const twoYearsLater = addYears(jan31, 2);
|
||||
console.log(twoYearsLater.getFullYear()); // 2026
|
||||
|
||||
// Days in February 2024 (leap year)
|
||||
console.log(daysInMonth(2024, 2)); // 29
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `FREQ_MAP: Record<string, Frequency>`
|
||||
|
||||
Maps uppercase RRULE frequency strings (`"DAILY"`, `"WEEKLY"`, `"MONTHLY"`, `"YEARLY"`) to their `Frequency` type values.
|
||||
|
||||
### `FREQ_RRULE: Record<Frequency, string>`
|
||||
|
||||
Inverse of `FREQ_MAP`. Maps `Frequency` type values back to uppercase RRULE strings.
|
||||
|
||||
### `DAY_NAME: Record<Weekday, string>`
|
||||
|
||||
Maps weekday indices (0–6) to two-letter RRULE abbreviations (`"SU"` … `"SA"`).
|
||||
|
||||
### `DAY_INDEX: Record<string, Weekday>`
|
||||
|
||||
Maps RRULE day abbreviations to weekday indices.
|
||||
|
||||
### `toRRuleDate(d: Date): string`
|
||||
|
||||
Format `d` as `YYYYMMDD` with zero-padded year, month, and day.
|
||||
|
||||
### `fromRRuleDate(s: string): Date`
|
||||
|
||||
Parse `s` (either `YYYYMMDD` or `YYYYMMDDTHHmmssZ`) into a local-midnight `Date`. The time and timezone suffix are stripped.
|
||||
|
||||
### `daysInMonth(year: number, month: number): number`
|
||||
|
||||
Returns the number of days in the given 1-indexed month. Mirrors the implementation in `calendar-core` to keep this module self-contained.
|
||||
|
||||
### `addDays(d: Date, n: number): Date`
|
||||
|
||||
Add `n` days to `d` using millisecond arithmetic.
|
||||
|
||||
### `addMonths(d: Date, n: number): Date`
|
||||
|
||||
Add `n` months with day clamping to the last valid day of the resulting month.
|
||||
|
||||
### `addYears(d: Date, n: number): Date`
|
||||
|
||||
Add `n` years by delegating to `addMonths(d, n * 12)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`addMonths` is the source of truth for month-end clamping in the recurrence engine. It handles month overflow (month > 12 rolls the year forward) and clamps the day to `daysInMonth(year, month)` using `Math.min`, ensuring that e.g. Jan 31 + 1 month → Feb 28/29 rather than Mar 2/3.
|
||||
|
||||
`daysInMonth` is intentionally duplicated from `calendar-core` rather than imported, to keep `recurrence-helpers` self-contained and free of cross-category imports.
|
||||
@@ -0,0 +1,102 @@
|
||||
# recurrence
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `recurrence` module implements a simplified iCalendar-compatible recurrence rule engine for `@suaveplan/datetime`. It provides parsing, formatting, and expansion of RRULE-style recurrence rules covering the most common scheduling patterns: daily, weekly, monthly, and yearly recurrences with interval, count, until, by-day-of-week, and by-day-of-month filters.
|
||||
|
||||
The design prioritises correctness and safety over exhaustiveness: it deliberately excludes the more exotic RFC 5545 features (BYSETPOS, BYHOUR, EXDATE, etc.) in order to maintain a clear, auditable code path that cannot produce runaway expansions.
|
||||
|
||||
## Features
|
||||
|
||||
- `parseRRule` — parse RRULE string into a typed `RecurrenceRule`
|
||||
- `formatRRule` — serialise `RecurrenceRule` back to RRULE string (round-trippable)
|
||||
- `nextOccurrence` — compute the next single occurrence after a given date
|
||||
- `expandRecurrence` — expand all occurrences with `count` or `until` bound (required)
|
||||
- Cooperative cancellation via `AbortSignal` (checked every 64 occurrences)
|
||||
- OTel histogram integration via `recordExpandRecurrence`
|
||||
- `clock` injection for deterministic tests
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { parseRRule, expandRecurrence } from "@suaveplan/datetime/recurrence";
|
||||
|
||||
const rule = parseRRule("FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=6");
|
||||
const dates = expandRecurrence(rule);
|
||||
console.log(dates.length); // 6
|
||||
console.log(dates[0].toDateString()); // next Monday/Wednesday/Friday
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
parseRRule,
|
||||
formatRRule,
|
||||
nextOccurrence,
|
||||
expandRecurrence,
|
||||
} from "@suaveplan/datetime/recurrence";
|
||||
|
||||
// Round-trip an RRULE string
|
||||
const raw = "FREQ=MONTHLY;BYMONTHDAY=15;COUNT=3";
|
||||
const rule = parseRRule(raw);
|
||||
const formatted = formatRRule(rule);
|
||||
console.log(formatted === raw); // true
|
||||
|
||||
// Deterministic expansion with a fake clock and abort signal
|
||||
const clock = () => new Date("2024-01-01T00:00:00Z").getTime();
|
||||
const controller = new AbortController();
|
||||
const dates = expandRecurrence(
|
||||
parseRRule("FREQ=DAILY;COUNT=365"),
|
||||
{ clock, signal: controller.signal }
|
||||
);
|
||||
console.log(dates.length); // 365
|
||||
|
||||
// Next weekly occurrence on Mondays
|
||||
const next = nextOccurrence(
|
||||
parseRRule("FREQ=WEEKLY;BYDAY=MO"),
|
||||
new Date("2024-03-13") // Wednesday
|
||||
);
|
||||
console.log(next?.toDateString()); // Mon Mar 18 2024
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `parseRRule(input: string): RecurrenceRule`
|
||||
|
||||
Parse a semicolon-delimited RRULE string. Supported fields:
|
||||
|
||||
- `FREQ` (required): `DAILY | WEEKLY | MONTHLY | YEARLY`
|
||||
- `INTERVAL`: positive integer, defaults to 1
|
||||
- `COUNT`: positive integer
|
||||
- `UNTIL`: `YYYYMMDD` or `YYYYMMDDTHHmmssZ`
|
||||
- `BYDAY`: comma-separated `SU|MO|TU|WE|TH|FR|SA`
|
||||
- `BYMONTHDAY`: comma-separated integers 1–31
|
||||
|
||||
Throws `DatetimeError("DATETIME.PARSE.INVALID_INPUT")` when `FREQ` is missing or unrecognised.
|
||||
|
||||
### `formatRRule(rule: RecurrenceRule): string`
|
||||
|
||||
Serialise a `RecurrenceRule` to an RRULE string. Output is round-trippable through `parseRRule`.
|
||||
|
||||
### `nextOccurrence(rule: RecurrenceRule, from: DateInput): Date | null`
|
||||
|
||||
Returns the first occurrence strictly after `from` that matches all filters. Returns `null` when the rule terminates (`until` exceeded or `count` exhausted) before generating a match.
|
||||
|
||||
### `expandRecurrence(rule: RecurrenceRule, opts?: RecurrenceOptions): Date[]`
|
||||
|
||||
Expand all occurrences into a `Date[]`. Requires `rule.count` or `rule.until`; throws `DatetimeError("DATETIME.RECURRENCE.INFINITE_EXPANSION")` when neither is set.
|
||||
|
||||
- `opts.clock` — injectable `Clock` to set the expansion start point (default: `Date.now`)
|
||||
- `opts.signal` — `AbortSignal` checked every 64 occurrences
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The expansion loop uses an internal safety limit of `count * 10 + 10000` to guard against degenerate filter combinations (e.g. `FREQ=MONTHLY;BYMONTHDAY=31;COUNT=100` in months shorter than 31 days). In practice this limit is never reached for well-formed rules.
|
||||
|
||||
Month arithmetic uses the same clamping strategy as `calendar.addDuration`, so `FREQ=MONTHLY;BYMONTHDAY=31` in February produces no occurrence for that month rather than overflowing into March.
|
||||
|
||||
`nextOccurrence` does not require `count` or `until` since it only generates a single result, but it applies a 3,650-iteration safety cap (~10 years of daily occurrences) before returning `null`.
|
||||
@@ -0,0 +1,84 @@
|
||||
# relative
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `relative` module provides human-readable relative-time formatting for `@suaveplan/datetime`. It converts the distance between two moments into concise English phrases like `"3 days ago"`, `"in 2 hours"`, or `"just now"`. The `clock` injection point makes all functions deterministic in tests, while the unified `formatRelative` entry point subsumes the common `timeAgo` / `timeUntil` aliases.
|
||||
|
||||
The module also exposes `diffInUnits`, a lower-level utility that returns the signed numeric difference between two `DateInput` values in any `DurationUnit`. This is useful when callers need the raw integer delta rather than a formatted string.
|
||||
|
||||
## Features
|
||||
|
||||
- `diffInUnits` — signed numeric difference in any `DurationUnit`
|
||||
- `formatRelative` — threshold-based English string (past and future)
|
||||
- `timeAgo` — convenience alias for past-relative formatting
|
||||
- `timeUntil` — convenience alias for future-relative formatting
|
||||
- Pluggable `Clock` for deterministic rendering in tests
|
||||
- `"just now"` threshold for differences under 45 seconds
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { timeAgo, timeUntil } from "@suaveplan/datetime/relative";
|
||||
|
||||
// Using wall clock
|
||||
console.log(timeAgo(new Date(Date.now() - 3 * 60_000))); // "3 minutes ago"
|
||||
console.log(timeUntil(new Date(Date.now() + 2 * 3600_000))); // "in 2 hours"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { diffInUnits, formatRelative } from "@suaveplan/datetime/relative";
|
||||
|
||||
// Deterministic with a fake clock
|
||||
const clock = () => new Date("2024-06-01T12:00:00Z").getTime();
|
||||
|
||||
const pastDate = "2024-05-28T12:00:00Z";
|
||||
console.log(formatRelative(pastDate, { clock })); // "4 days ago"
|
||||
console.log(diffInUnits(pastDate, "2024-06-01T12:00:00Z", "days")); // 4
|
||||
|
||||
// Sub-unit precision
|
||||
const oneHourAgo = new Date(clock() - 3723_000); // 1h 2m 3s ago
|
||||
console.log(formatRelative(oneHourAgo, { clock })); // "1 hour ago"
|
||||
console.log(diffInUnits(oneHourAgo, new Date(clock()), "seconds")); // 3723
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `diffInUnits(a: DateInput, b: DateInput, unit: DurationUnit): number`
|
||||
|
||||
Returns `floor((b − a) / unitMs)` truncated toward zero. Positive when `b > a`.
|
||||
|
||||
- `a` — start date (any `DateInput`)
|
||||
- `b` — end date (any `DateInput`)
|
||||
- `unit` — one of the `DurationUnit` values
|
||||
|
||||
### `formatRelative(date: DateInput, opts?: RelativeTimeOptions): string`
|
||||
|
||||
Format the difference between `date` and `opts.clock()` (default: `Date.now()`).
|
||||
|
||||
- Distances under 45 seconds → `"just now"`
|
||||
- Distances under 45 minutes → `"N seconds ago"` / `"in N seconds"`
|
||||
- Distances under 22 hours → `"N minutes ago"` / `"in N minutes"`
|
||||
- Distances under 26 days → `"N hours ago"` / `"in N hours"`
|
||||
- Distances under 11 months → `"N days ago"` / `"in N days"`
|
||||
- Otherwise → `"N months ago"` / `"in N months"` or `"N years ago"` / `"in N years"`
|
||||
|
||||
### `timeAgo(date: DateInput, opts?: RelativeTimeOptions): string`
|
||||
|
||||
Alias for `formatRelative`. Intended for past dates, but works symmetrically.
|
||||
|
||||
### `timeUntil(date: DateInput, opts?: RelativeTimeOptions): string`
|
||||
|
||||
Alias for `formatRelative`. Intended for future dates, but works symmetrically.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Average month (30.4375 days) and average year (365.25 days) constants are used for the coarse `months` / `years` thresholds. These are approximations intentionally — precise calendar arithmetic for display strings would add complexity without improving user experience. Use `diffInUnits("months")` together with calendar arithmetic when you need exact month counts.
|
||||
|
||||
The threshold table mirrors the conventions used by popular libraries like `date-fns` and `moment.js` so that migrating code produces the same user-visible strings.
|
||||
|
||||
The `precision` field in `RelativeTimeOptions` is reserved for future use to cap the label at a specific unit (e.g. only show up to "days", never "hours"). Currently it is accepted but not applied in the implementation.
|
||||
@@ -0,0 +1,69 @@
|
||||
# datetime.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared type definitions for the `@suaveplan/datetime` package. This module is the single source of truth for the shapes that flow through business-day arithmetic, calendar math, recurrence expansion, and relative-time formatting. Co-locating these definitions in one flat file keeps the feature modules focused on behaviour and keeps cross-module contracts obvious at a glance.
|
||||
|
||||
## Features
|
||||
|
||||
- Zero runtime cost — all exports are pure TypeScript types
|
||||
- Branded `Timezone` string for IANA names that prevents accidental raw-string injection
|
||||
- `DurationUnit` union aligned with the full `years | months | weeks | days | hours | minutes | seconds | milliseconds` vocabulary used across the library
|
||||
- `BusinessDayOptions` and `RelativeTimeOptions` carry a pluggable `Clock` from `@suaveplan/types` so every consumer can be driven by a deterministic fake in tests
|
||||
- `RecurrenceRule` subset aligned with the iCalendar/RRULE vocabulary (`freq`, `interval`, `count`, `until`, `byDay`, `byMonthDay`) without the full RFC 5545 complexity
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { Duration, Frequency, Weekday } from "@suaveplan/datetime/types/datetime.types";
|
||||
|
||||
const oneWeek: Duration = { days: 7 };
|
||||
const monday: Weekday = 1;
|
||||
const rule: Frequency = "weekly";
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
BusinessDayOptions,
|
||||
RecurrenceRule,
|
||||
Timezone,
|
||||
} from "@suaveplan/datetime/types/datetime.types";
|
||||
|
||||
const businessOptions: BusinessDayOptions = {
|
||||
holidays: [new Date("2026-01-01")],
|
||||
weekendDays: [0, 6],
|
||||
clock: () => Date.UTC(2026, 0, 1),
|
||||
};
|
||||
|
||||
const weeklyStandup: RecurrenceRule = {
|
||||
freq: "weekly",
|
||||
interval: 1,
|
||||
byDay: [1, 3, 5],
|
||||
count: 52,
|
||||
};
|
||||
|
||||
const nycTz = "America/New_York" as Timezone;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `DateInput` — union of `Date | string | number`; accepted by parser helpers.
|
||||
- `DurationUnit` — named calendar/time units.
|
||||
- `Duration` — partial record of `DurationUnit → number`.
|
||||
- `Weekday` — numeric index `0..6` matching `Date.prototype.getDay()`.
|
||||
- `DayAbbreviation` — three-letter English abbreviations.
|
||||
- `Frequency` — `"daily" | "weekly" | "monthly" | "yearly"` recurrence cadence.
|
||||
- `Timezone` — branded IANA-name string.
|
||||
- `CalendarDay` / `CalendarMonth` — grid shapes returned by `getCalendarMonth`.
|
||||
- `BusinessDayOptions` — options bundle for business-day arithmetic with optional holidays, custom weekend days, clock injection, and `AbortSignal`.
|
||||
- `RelativeTimeOptions` — locale, clock, and precision knobs for `formatRelativeTime`.
|
||||
- `RecurrenceRule` — RRULE-like shape consumed by `expandRecurrence`.
|
||||
- `RecurrenceOptions` — signal + clock bundle for the expansion loop.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All interfaces deliberately avoid class-based modelling so they can round-trip through JSON and be used as wire-format types by downstream services. The `Timezone` brand is enforced only at compile time; runtime validation lives in `asTimezone()` in the calendar module, which checks against `Intl.supportedValuesOf('timeZone')`. Consumers should never cast a raw `string` to `Timezone` — always construct via the validator.
|
||||
|
||||
The `Clock` and `Rng` injections come from `@suaveplan/types` (the types-only foundation package) rather than being redefined here, which prevents structural drift between `datetime`, `timing`, `retry`, and every other package that takes a clock.
|
||||
Reference in New Issue
Block a user