daymath 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +46 -58
  2. package/index.d.ts +10 -4
  3. package/index.js +25 -9
  4. package/package.json +5 -3
package/README.md CHANGED
@@ -1,95 +1,83 @@
1
1
  # daymath
2
2
 
3
- Calendar date math for **ISO 8601** `YYYY-MM-DD` strings. **date-fns-shaped** names. **Temporal.PlainDate** under the hood.
3
+ [![npm](https://img.shields.io/npm/v/daymath.svg)](https://www.npmjs.com/package/daymath)
4
+ [![license](https://img.shields.io/npm/l/daymath.svg)](./LICENSE)
5
+ [![node](https://img.shields.io/node/v/daymath.svg)](https://www.npmjs.com/package/daymath)
6
+
7
+ **ISO 8601** calendar day math. **date-fns-shaped** names. **Temporal.PlainDate** under the hood.
4
8
 
5
9
  No `Date`. No time zones. No silent “local now.”
6
10
 
11
+ [**Play in the browser →**](https://leemr.github.io/daymath/) · [npm](https://www.npmjs.com/package/daymath) · [FUTURE.md](./FUTURE.md)
12
+
7
13
  ```bash
8
14
  npm install daymath
9
15
  ```
10
16
 
11
- ## Why
12
-
13
- `Date` is a timestamp. Calendar work (“add one month”, “days between hire and start”) is not. This package only does plain calendar days.
14
-
15
- ## Usage
16
-
17
17
  ```js
18
- import {
19
- addDays,
20
- addMonths,
21
- differenceInDays,
22
- isBefore,
23
- isSameDay,
24
- startOfMonth,
25
- eachDayOfInterval,
26
- } from 'daymath'
27
-
28
- addDays('2026-08-06', 1) // '2026-08-07'
29
- addMonths('2026-01-31', 1) // '2026-02-28' (constrain)
18
+ import { addDays, addMonths, differenceInDays, isSameDay } from 'daymath'
19
+
20
+ addDays('2026-08-06', 1) // '2026-08-07'
21
+ addMonths('2026-01-31', 1) // '2026-02-28'
30
22
  differenceInDays('2026-08-06', '2026-08-01') // 5
31
- isBefore('2026-08-05', '2026-08-06') // true
32
- isSameDay('2026-08-06', '2026-08-06') // true (alias of isEqual)
33
- startOfMonth('2026-08-06') // '2026-08-01'
34
- eachDayOfInterval({
35
- start: '2026-08-05',
36
- end: '2026-08-07',
37
- }) // ['2026-08-05', '2026-08-06', '2026-08-07']
23
+ isSameDay('2026-08-06', '2026-08-06') // true
38
24
  ```
39
25
 
40
- **Inputs:** ISO 8601 `YYYY-MM-DD` or `Temporal.PlainDate`.
41
- **Outputs:** always `YYYY-MM-DD` string (for math helpers).
26
+ ## Why
27
+
28
+ `Date` is a timestamp. Hire dates, passport expiry, trip days are **calendar** values. daymath only does plain days as ISO strings.
29
+
30
+ | In | Out |
31
+ |----|-----|
32
+ | `YYYY-MM-DD` or expanded `±YYYYYY-MM-DD` | same forms (Temporal `toString`) |
33
+ | or `Temporal.PlainDate` | string |
42
34
 
43
- `Date` throws. Time-bearing strings throw. Sloppy forms like `2026-8-6` throw.
35
+ `Date` **throws** (including `isValid`). `isValid('asdf')` `false`.
44
36
 
45
- ## date-fns parity notes
37
+ ## date-fns parity (names, not `Date`)
46
38
 
47
39
  | Topic | daymath |
48
40
  |-------|---------|
49
- | Value type | `YYYY-MM-DD` string (not `Date`) |
50
- | `isSameDay` | Alias of `isEqual` (same calendar day) |
51
- | `getMonth` / `setMonth` | **0–11** like Date/date-fns (0 = January) |
52
- | `getDay` | **0–6** like Date/date-fns (0 = Sunday) |
53
- | `weekStartsOn` | `0` = Sunday … `6` = Saturday (default `0`) |
54
- | Intervals | `{ start, end }` inclusive for `each*` / `isWithin` / `clamp` |
55
- | `areIntervalsOverlapping` | Default `{ inclusive: false }` (date-fns); pass `true` for closed |
41
+ | Values | ISO day **strings**, not `Date` |
42
+ | `isSameDay` | Alias of `isEqual` |
43
+ | `isValid` | Valid daymath day; **`Date` throws** |
44
+ | `getMonth` / `setMonth` | **0–11** (0 = January) |
45
+ | `getDay` | **0–6** (0 = Sunday) |
46
+ | `weekStartsOn` | default `0` (Sunday) |
47
+ | Intervals | `{ start, end }` |
56
48
 
57
- ## API (0.2)
49
+ ## API
58
50
 
59
- ### Parse / format
60
- `parse` · `format` · `isValid`
51
+ **Parse** `parse` · `format` · `isValid`
61
52
 
62
- ### Add / sub
63
- `addDays` / `subDays` · `addWeeks` / `subWeeks` · `addMonths` / `subMonths` · `addYears` / `subYears` · `addQuarters` / `subQuarters`
53
+ **Add/sub** — Days · Weeks · Months · Years · Quarters
64
54
 
65
- ### Get / set
66
- `getYear` · `getMonth` · `getDate` · `getDay` · `getDayOfYear` · `getDaysInMonth` · `getQuarter` · `isLeapYear`
67
- `setYear` · `setMonth` · `setDate`
55
+ **Get/set** — `getYear` · `getMonth` · `getDate` · `getDay` · `getDayOfYear` · `getDaysInMonth` · `getQuarter` · `isLeapYear` · `setYear` · `setMonth` · `setDate`
68
56
 
69
- ### Start / end
70
- `startOfMonth` / `endOfMonth` · `startOfYear` / `endOfYear` · `startOfQuarter` / `endOfQuarter` · `startOfWeek` / `endOfWeek`
57
+ **Bounds** `startOf`/`endOf` Month · Year · Quarter · Week
71
58
 
72
- ### Differences
73
- `differenceInDays` · `differenceInWeeks` · `differenceInMonths` · `differenceInCalendarMonths` · `differenceInYears` · `differenceInCalendarYears` · `differenceInQuarters` · `differenceInCalendarQuarters`
59
+ **Diffs** — Days · Weeks · Months · CalendarMonths · Years · CalendarYears · Quarters · CalendarQuarters
74
60
 
75
- ### Compare
76
- `isBefore` · `isAfter` · `isEqual` · `isSameDay` · `isSameWeek` · `isSameMonth` · `isSameYear` · `isSameQuarter` · `compareAsc` · `compareDesc` · `min` · `max`
61
+ **Compare** — `isBefore` · `isAfter` · `isEqual` · `isSameDay` · `isSameWeek` · Month · Year · Quarter · `compareAsc` · `compareDesc` · `min` · `max`
77
62
 
78
- ### Weekday / month edges
79
- `isSunday`…`isSaturday` · `isWeekend` · `isFirstDayOfMonth` · `isLastDayOfMonth`
63
+ **Weekday** — `isSunday`…`isSaturday` · `isWeekend` · first/last day of month
80
64
 
81
- ### Intervals
82
- `eachDayOfInterval` · `eachMonthOfInterval` · `eachYearOfInterval` · `isWithinInterval` · `clamp` · `areIntervalsOverlapping`
65
+ **Intervals** — `eachDayOfInterval` · `eachMonthOfInterval` · `eachYearOfInterval` · `isWithinInterval` · `clamp` · `areIntervalsOverlapping`
83
66
 
84
- Amounts must be finite integers.
67
+ Amounts are finite integers.
85
68
 
86
69
  ## Temporal
87
70
 
88
71
  Uses global `Temporal` when present; otherwise [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill).
89
72
 
90
- ## Types
73
+ ## Types & tests
91
74
 
92
- Ships `index.d.ts` (no TypeScript compile step). Source is plain JS.
75
+ Plain JS + `index.d.ts` (no compile step).
76
+
77
+ ```bash
78
+ npm test
79
+ npm run test:coverage # 100% lines on index.js
80
+ ```
93
81
 
94
82
  ## License
95
83
 
package/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Temporal } from 'temporal-polyfill'
2
2
 
3
3
  /**
4
- * Calendar day input: ISO 8601 `YYYY-MM-DD` string, or a Temporal.PlainDate.
5
- * `Date` is rejected at runtime.
4
+ * Calendar day input: ISO 8601 day string, or a Temporal.PlainDate.
5
+ * - `YYYY-MM-DD` (years 0000–9999)
6
+ * - expanded `±YYYYYY-MM-DD` (e.g. `+010000-01-01`)
7
+ * `Date` is rejected at runtime (TypeError).
6
8
  */
7
9
  export type DayInput = string | Temporal.PlainDate
8
10
 
@@ -19,12 +21,16 @@ export type WeekOptions = {
19
21
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
20
22
  }
21
23
 
24
+ /**
25
+ * True for a valid daymath day string / PlainDate.
26
+ * Invalid strings → false. `Date` → throws TypeError (not a quiet false).
27
+ */
22
28
  export function isValid(value: unknown): boolean
23
29
 
24
- /** Validate / normalize to ISO 8601 `YYYY-MM-DD`. */
30
+ /** Validate / normalize to ISO 8601 day string (Temporal `toString` form). */
25
31
  export function parse(date: DayInput): string
26
32
 
27
- /** Format as `YYYY-MM-DD` (only pattern supported). */
33
+ /** Format as ISO day (only `yyyy-MM-dd` / `YYYY-MM-DD` patterns supported). */
28
34
  export function format(date: DayInput, pattern?: 'yyyy-MM-dd' | 'YYYY-MM-DD'): string
29
35
 
30
36
  export function addDays(date: DayInput, amount: number): string
package/index.js CHANGED
@@ -1,8 +1,16 @@
1
- /** daymath — calendar date math (YYYY-MM-DD). date-fns-shaped. No Date / time zones. */
1
+ /** daymath — calendar date math (ISO 8601 day). date-fns-shaped. No Date / time zones. */
2
2
  import { Temporal as TemporalPolyfill } from 'temporal-polyfill'
3
3
 
4
4
  const Temporal = globalThis.Temporal ?? TemporalPolyfill
5
5
 
6
+ /**
7
+ * ISO 8601 calendar day string:
8
+ * - `YYYY-MM-DD` (years 0000–9999)
9
+ * - expanded `±YYYYYY-MM-DD` (Temporal form, e.g. `+010000-01-01`)
10
+ */
11
+ const ISO_DAY =
12
+ /^(?:[+-]\d{6}|\d{4})-\d{2}-\d{2}$/
13
+
6
14
  /** @typedef {string | Temporal.PlainDate} DayInput */
7
15
  /**
8
16
  * @typedef {object} Interval
@@ -17,7 +25,7 @@ const Temporal = globalThis.Temporal ?? TemporalPolyfill
17
25
  // ─── core conversion ───────────────────────────────────────────────
18
26
 
19
27
  /**
20
- * Reject Date and non-calendar values. Accept YYYY-MM-DD string or PlainDate.
28
+ * Reject Date and non-calendar values. Accept ISO day string or PlainDate.
21
29
  * @param {unknown} value
22
30
  * @param {string} label
23
31
  * @returns {Temporal.PlainDate}
@@ -25,14 +33,13 @@ const Temporal = globalThis.Temporal ?? TemporalPolyfill
25
33
  function toPlainDate(value, label = 'date') {
26
34
  if (value instanceof Date) {
27
35
  throw new TypeError(
28
- `daymath: Date is not allowed for ${label} (pass YYYY-MM-DD string)`,
36
+ `daymath: Date is not allowed for ${label} (pass ISO 8601 day string)`,
29
37
  )
30
38
  }
31
39
  if (typeof value === 'string') {
32
- // Strict ISO 8601 calendar date: YYYY-MM-DD only.
33
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
40
+ if (!ISO_DAY.test(value)) {
34
41
  throw new RangeError(
35
- `daymath: ${label} must be YYYY-MM-DD (got ${JSON.stringify(value)})`,
42
+ `daymath: ${label} must be ISO 8601 day YYYY-MM-DD or ±YYYYYY-MM-DD (got ${JSON.stringify(value)})`,
36
43
  )
37
44
  }
38
45
  try {
@@ -47,7 +54,7 @@ function toPlainDate(value, label = 'date') {
47
54
  return value
48
55
  }
49
56
  throw new TypeError(
50
- `daymath: ${label} must be YYYY-MM-DD string or Temporal.PlainDate`,
57
+ `daymath: ${label} must be ISO 8601 day string or Temporal.PlainDate`,
51
58
  )
52
59
  }
53
60
 
@@ -105,8 +112,17 @@ function toInterval(interval) {
105
112
 
106
113
  // ─── parse / format / valid ────────────────────────────────────────
107
114
 
108
- /** @param {unknown} value */
115
+ /**
116
+ * True if value is a valid daymath day (ISO day string or PlainDate).
117
+ * Invalid strings → false. `Date` → throws (not a quiet false — swap trap).
118
+ * @param {unknown} value
119
+ */
109
120
  export function isValid(value) {
121
+ if (value instanceof Date) {
122
+ throw new TypeError(
123
+ 'daymath: Date is not allowed for isValid (pass ISO 8601 day string)',
124
+ )
125
+ }
110
126
  try {
111
127
  toPlainDate(value)
112
128
  return true
@@ -116,7 +132,7 @@ export function isValid(value) {
116
132
  }
117
133
 
118
134
  /**
119
- * Validate / normalize a calendar day string (ISO 8601 YYYY-MM-DD).
135
+ * Validate / normalize an ISO 8601 calendar day string.
120
136
  * @param {DayInput} date
121
137
  * @returns {string}
122
138
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "daymath",
3
- "version": "0.2.0",
4
- "description": "Calendar date math (YYYY-MM-DD / PlainDate). date-fns-shaped. No time zones.",
3
+ "version": "0.2.2",
4
+ "description": "Calendar date math (ISO 8601 day / PlainDate). date-fns-shaped. No time zones.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
7
  "types": "./index.d.ts",
@@ -18,8 +18,10 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "test": "node --test test.js",
21
- "prepublishOnly": "npm test"
21
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-include=index.js test.js",
22
+ "prepublishOnly": "npm run test:coverage"
22
23
  },
24
+
23
25
  "keywords": [
24
26
  "date",
25
27
  "calendar",