@zerotal/arch 1.7.0

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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
package/docs/carbon.md ADDED
@@ -0,0 +1,830 @@
1
+ ---
2
+ title: Carbon & CarbonInterval
3
+ description: Timezone-aware, immutable date-times and durations — creating, reading, arithmetic, differences, formatting, intervals, and testing on one page.
4
+ ---
5
+
6
+ # Carbon & CarbonInterval
7
+
8
+ Carbon is an immutable date-time value object backed by the TC39 Temporal API
9
+ (`Temporal.ZonedDateTime`); CarbonInterval is its companion duration type. Every
10
+ modifier returns a **new** instance, so values are safe to share, cache, and pass
11
+ around without defensive copying.
12
+
13
+ Reach for Carbon instead of the native `Date` whenever you need timezone-aware
14
+ arithmetic, fluent formatting, or human-readable diffs.
15
+
16
+ ## Getting Started
17
+
18
+ Both classes are exported from `zerotal/carbon` — no package to install or provider
19
+ to register, they are part of the core runtime.
20
+
21
+ ```typescript
22
+ // in a controller, model, or anywhere in your app
23
+ import { Carbon, CarbonInterval } from "zerotal/carbon";
24
+ ```
25
+
26
+ ## Creating a Carbon
27
+
28
+ The constructor accepts a string, a millisecond timestamp, a native `Date`, a
29
+ Temporal type, or another Carbon. A second argument sets the timezone.
30
+
31
+ ```typescript
32
+ // anywhere in your app
33
+ // Current date and time (system timezone)
34
+ const now = new Carbon();
35
+ const now2 = Carbon.now();
36
+
37
+ // With an explicit timezone
38
+ const inNY = Carbon.now("America/New_York");
39
+ const inLondon = new Carbon(new Date(), "Europe/London");
40
+
41
+ // From a string
42
+ const d1 = new Carbon("2026-06-15");
43
+ const d2 = new Carbon("2026-06-15T09:30:00");
44
+ const d3 = new Carbon("2026-06-15T09:30:00+02:00[Europe/Paris]"); // ZonedDateTime string
45
+
46
+ // From a Unix timestamp in milliseconds
47
+ const d4 = new Carbon(1_718_400_000_000);
48
+
49
+ // From a Unix timestamp (static factories)
50
+ const d5 = Carbon.fromTimestamp(1_718_400); // seconds
51
+ const d6 = Carbon.fromMilliseconds(1_718_400_000); // milliseconds
52
+
53
+ // From a native Date
54
+ const d7 = new Carbon(new Date());
55
+
56
+ // From a Temporal type (Instant, PlainDateTime, PlainDate, ZonedDateTime)
57
+ const d8 = new Carbon(Temporal.Now.instant());
58
+ ```
59
+
60
+ Named static factories cover the common boundary cases (all accept an optional
61
+ timezone string):
62
+
63
+ ```typescript
64
+ // anywhere in your app
65
+ Carbon.today(); // today at 00:00:00
66
+ Carbon.tomorrow(); // tomorrow at 00:00:00
67
+ Carbon.yesterday(); // yesterday at 00:00:00
68
+ Carbon.startOfMonth(); // first day of current month at 00:00:00
69
+ Carbon.endOfMonth(); // last day of current month at 23:59:59.999…
70
+ Carbon.startOfWeek(); // Monday of current ISO week at 00:00:00
71
+ Carbon.endOfWeek(); // Sunday of current ISO week at 23:59:59.999…
72
+ Carbon.startOfYear(); // 1 Jan at 00:00:00
73
+ Carbon.endOfYear(); // 31 Dec at 23:59:59.999…
74
+ Carbon.create(input); // same as new Carbon(input)
75
+ ```
76
+
77
+ ## Immutability
78
+
79
+ Every modifier returns a **new** Carbon. Chain freely — the base instance never
80
+ changes.
81
+
82
+ ```typescript
83
+ // anywhere in your app
84
+ const base = new Carbon("2026-01-15");
85
+ const nextWeek = base.addDays(7);
86
+ const deadline = base.addMonths(1).startOfDay();
87
+
88
+ base.format("YYYY-MM-DD"); // '2026-01-15' — unchanged
89
+ nextWeek.format("YYYY-MM-DD"); // '2026-01-22'
90
+ deadline.format("YYYY-MM-DD"); // '2026-02-15'
91
+ ```
92
+
93
+ ## Timezones
94
+
95
+ A Carbon carries its timezone; converting produces a new instance pointing at the
96
+ same instant.
97
+
98
+ ```typescript
99
+ // anywhere in your app
100
+ // Read the timezone of an instance
101
+ const tz = Carbon.now("Asia/Tokyo").timezone; // 'Asia/Tokyo'
102
+
103
+ // Convert to a different timezone (same instant, different local time)
104
+ const utc = new Carbon("2026-06-15T12:00:00", "UTC");
105
+ const paris = utc.inTimezone("Europe/Paris"); // 14:00 (UTC+2)
106
+ const tokyo = utc.inTimezone("Asia/Tokyo"); // 21:00 (UTC+9)
107
+
108
+ // Static factories accept a timezone
109
+ const ny = Carbon.today("America/New_York");
110
+ ```
111
+
112
+ ## Reading a Carbon
113
+
114
+ ### Getters
115
+
116
+ Reading a Carbon never changes it. Calendar fields are plain properties, so they
117
+ carry no parentheses; the three counts that depend on the surrounding calendar are
118
+ methods, because they compute an answer rather than expose a stored field.
119
+
120
+ ```typescript
121
+ // anywhere in your app
122
+ const d = new Carbon("2026-06-15 09:30:45.123");
123
+
124
+ d.year; // 2026
125
+ d.month; // 6 (1-indexed)
126
+ d.day; // 15
127
+ d.hour; // 9
128
+ d.minute; // 30
129
+ d.second; // 45
130
+ d.millisecond; // 123
131
+ d.microsecond; // 0
132
+ d.nanosecond; // 0
133
+
134
+ d.dayOfWeek; // 1 (ISO 8601: 1 = Monday … 7 = Sunday)
135
+ d.dayOfYear; // 166
136
+ d.weekOfYear; // 25 (ISO week number)
137
+
138
+ d.monthName; // 'June'
139
+ d.dayName; // 'Monday'
140
+
141
+ d.timezone; // 'America/Chicago' (system tz)
142
+
143
+ d.daysInMonth(); // 30
144
+ d.daysInYear(); // 365
145
+ d.weeksInYear(); // 52
146
+ ```
147
+
148
+ This `d` is the sample instance the arithmetic and formatting examples below reuse.
149
+
150
+ Two of these follow ISO 8601 where the native `Date` does not, and both differences
151
+ bite silently — the code runs and the answer is wrong:
152
+
153
+ | Field | Carbon | Native `Date` |
154
+ | ----------- | ----------------------- | ------------------------ |
155
+ | `month` | 1 = January | `getMonth()` 0 = January |
156
+ | `dayOfWeek` | 1 = Monday … 7 = Sunday | `getDay()` 0 = Sunday |
157
+
158
+ Getters read the value in the instance's own timezone, so the same instant viewed
159
+ in two zones reports different fields. That is the intended behaviour, and the
160
+ reason to compare instants with the predicates below rather than by pulling fields
161
+ out and comparing them by hand.
162
+
163
+ ### Predicates
164
+
165
+ ```typescript
166
+ // anywhere in your app
167
+ d.isToday();
168
+ d.isTomorrow();
169
+ d.isYesterday();
170
+
171
+ d.isPast(); // strictly before now
172
+ d.isFuture(); // strictly after now
173
+
174
+ d.isWeekend(); // Saturday (6) or Sunday (7) in ISO dayOfWeek
175
+ d.isWeekday();
176
+
177
+ d.isLeapYear();
178
+
179
+ d.isBefore(other);
180
+ d.isAfter(other);
181
+ d.isEqual(other); // same instant
182
+
183
+ d.isSameDay(other); // same calendar date
184
+ d.isSameMonth(other); // same year + month
185
+ d.isSameYear(other);
186
+
187
+ d.isBetween(start, end); // inclusive (default)
188
+ d.isBetween(start, end, false); // exclusive
189
+ ```
190
+
191
+ `isPast()` and `isFuture()` are strict: an instant equal to now satisfies neither.
192
+
193
+ > **Warning** — Comparing two Carbons with `===` compares object identity and is
194
+ > always false. Compare with the predicates above (`isEqual`, `isBefore`,
195
+ > `isAfter`) or on a serialised form.
196
+
197
+ ### Comparing instants and comparing dates
198
+
199
+ The comparison predicates fall into two families, and choosing across them is the
200
+ most common source of date bugs:
201
+
202
+ - **Instant comparisons** — `isBefore`, `isAfter`, `isEqual` — ask which moment
203
+ came first on the world's timeline. Timezones are already accounted for, so two
204
+ values written in different zones compare correctly.
205
+ - **Calendar comparisons** — `isSameDay`, `isSameMonth`, `isSameYear` — ask whether
206
+ two values fall in the same named period, as read in their own timezones.
207
+
208
+ The two disagree exactly when a timezone boundary falls between the values. Two
209
+ instants a minute apart can land on different calendar days; the same instant read
210
+ in Tokyo and in Chicago routinely does. So when the question is "did this happen
211
+ before that", reach for `isBefore`; when it is "does this belong to today's
212
+ report", reach for `isSameDay`.
213
+
214
+ `isBetween` includes both endpoints by default. Pass `false` as the third argument
215
+ for an exclusive range — worth doing when you are bucketing values into adjacent
216
+ ranges, where inclusive bounds would place a boundary value in both buckets.
217
+
218
+ ## Arithmetic
219
+
220
+ ### Add and subtract
221
+
222
+ All arithmetic returns a new Carbon.
223
+
224
+ ```typescript
225
+ // anywhere in your app
226
+ d.addNanoseconds(1) d.subtractNanoseconds(1)
227
+ d.addMicroseconds(1) d.subtractMicroseconds(1)
228
+ d.addMilliseconds(500) d.subtractMilliseconds(500)
229
+ d.addSeconds(30) d.subtractSeconds(30)
230
+ d.addMinutes(15) d.subtractMinutes(15)
231
+ d.addHours(2) d.subtractHours(2)
232
+ d.addDays(7) d.subtractDays(7)
233
+ d.addWeeks(2) d.subtractWeeks(2)
234
+ d.addMonths(3) d.subtractMonths(3)
235
+ d.addYears(1) d.subtractYears(1)
236
+ d.addDecades(1) d.subtractDecades(1)
237
+ d.addCenturies(1) d.subtractCenturies(1)
238
+ d.addMillennia(1) d.subtractMillennia(1)
239
+ ```
240
+
241
+ Short `sub*` aliases exist for the common units (`subDays`, `subMonths`, etc.).
242
+
243
+ Passing a negative amount is the same as subtracting, so one call site can move in
244
+ either direction from a computed value without branching.
245
+
246
+ To add a [CarbonInterval](#carboninterval), use `add()` / `subtract()`:
247
+
248
+ ```typescript
249
+ // anywhere in your app
250
+ const interval = CarbonInterval.days(3).andHours(6);
251
+
252
+ d.add(interval); // new Carbon = d + 3d 6h
253
+ d.subtract(interval); // new Carbon = d - 3d 6h
254
+ ```
255
+
256
+ ### Calendar units clamp, and do not reverse
257
+
258
+ Months and years are calendar units rather than fixed spans, so adding one lands
259
+ on the same day number in the target month — and clamps when that day does not
260
+ exist there:
261
+
262
+ ```typescript
263
+ new Carbon("2026-01-31").addMonths(1); // → 2026-02-28
264
+ new Carbon("2024-02-29").addYears(1); // → 2025-02-28
265
+ ```
266
+
267
+ Clamping discards information, which makes month arithmetic asymmetric. Adding a
268
+ month and taking it back does not always return the original date:
269
+
270
+ ```typescript
271
+ new Carbon("2026-01-31").addMonths(1).subtractMonths(1); // → 2026-01-28
272
+ ```
273
+
274
+ This is correct calendar behaviour rather than a rounding bug, and it matters in
275
+ two places worth guarding. When stepping through months in a loop, advance from a
276
+ fixed anchor rather than from the previous result, or the day of month drifts
277
+ earlier with every iteration. When a monthly billing date must stay on the 31st,
278
+ keep the intended day number alongside the date rather than trying to recover it
279
+ from the last value computed.
280
+
281
+ Days, hours, and the smaller units carry no such ambiguity — they are exact spans
282
+ and always reverse cleanly.
283
+
284
+ ### Boundary methods
285
+
286
+ Snap to the start or end of a time period — all return a new Carbon.
287
+
288
+ ```typescript
289
+ // anywhere in your app
290
+ d.startOfMinute(); // :00.000
291
+ d.endOfMinute(); // :59.999999999
292
+
293
+ d.startOfHour(); // hh:00:00.000
294
+ d.endOfHour(); // hh:59:59.999999999
295
+
296
+ d.startOfDay(); // 00:00:00.000
297
+ d.endOfDay(); // 23:59:59.999999999
298
+
299
+ d.startOfWeek(); // Monday 00:00:00 (ISO: Mon–Sun)
300
+ d.endOfWeek(); // Sunday 23:59:59.999999999
301
+
302
+ d.startOfMonth(); // 1st of month, 00:00:00
303
+ d.endOfMonth(); // last day of month, 23:59:59.999999999
304
+
305
+ d.startOfYear(); // 1 Jan 00:00:00
306
+ d.endOfYear(); // 31 Dec 23:59:59.999999999
307
+
308
+ d.startOfDecade(); // first day of decade (e.g. 2020) 00:00:00
309
+ d.endOfDecade(); // last day of decade (e.g. 2029) 23:59:59.999…
310
+
311
+ d.startOfCentury(); // first day of century 00:00:00
312
+ d.endOfCentury(); // last day of century 23:59:59.999…
313
+ ```
314
+
315
+ Weeks follow ISO 8601, so `startOfWeek()` moves back to Monday. Applied to a
316
+ Sunday it therefore travels six days _backwards_ rather than forward, because that
317
+ Sunday closes the week instead of opening it.
318
+
319
+ The `end*` methods land on the last representable nanosecond of the period rather
320
+ than on the start of the next one, which is what makes them safe as the upper
321
+ bound of an inclusive range: `between(startOfDay(), endOfDay())` captures the whole
322
+ day without also catching midnight the next morning.
323
+
324
+ ### Field setters
325
+
326
+ The `with` prefix signals a new instance is returned.
327
+
328
+ ```typescript
329
+ // anywhere in your app
330
+ d.withYear(2030);
331
+ d.withMonth(12); // 1-indexed
332
+ d.withDay(1);
333
+ d.withHour(9);
334
+ d.withMinute(0);
335
+ d.withSecond(0);
336
+ d.withMillisecond(0);
337
+ d.withMicrosecond(0);
338
+ d.withNanosecond(0);
339
+ d.withTime(9, 30); // hours + minutes (seconds and ms reset to 0)
340
+ d.withTime(9, 30, 0, 0); // hours, minutes, seconds, milliseconds
341
+ ```
342
+
343
+ Setters replace a field outright instead of shifting by an amount — the difference
344
+ between "the 15th of this month" (`withDay(15)`) and "a fortnight from now"
345
+ (`addDays(14)`). Reach for `withTime()` when normalising a timestamp to a fixed
346
+ time of day, since it resets the smaller fields for you rather than needing a chain
347
+ of setters.
348
+
349
+ ## Differences
350
+
351
+ All numeric diff methods return a **number** — positive when `this` is after
352
+ `other`.
353
+
354
+ ```typescript
355
+ // anywhere in your app
356
+ const a = new Carbon("2026-01-01");
357
+ const b = new Carbon("2026-06-15");
358
+
359
+ b.diffInMilliseconds(a); // ~14,515,200,000 (raw millisecond difference)
360
+ b.diffInSeconds(a);
361
+ b.diffInMinutes(a);
362
+ b.diffInHours(a);
363
+ b.diffInDays(a); // ~165.5
364
+ b.diffInWeeks(a); // ~23.6
365
+ b.diffInMonths(a); // 5 (calendar months: year×12 + month delta)
366
+ b.diffInYears(a); // ~0.42
367
+ ```
368
+
369
+ > **Note** — `diffInMonths` counts calendar months (`year×12 + month` delta), not
370
+ > elapsed time, so `diffInYears` derives from it. For a fractional, instant-based
371
+ > measure use `diffInDays` or `diffAsCarbonInterval`.
372
+
373
+ For a calendar-aware breakdown use `diffAsCarbonInterval`, which delegates to
374
+ Temporal's `until()`:
375
+
376
+ ```typescript
377
+ // anywhere in your app
378
+ const age = birthday.diffAsCarbonInterval(Carbon.now(), "year");
379
+ // → CarbonInterval { years: 28, months: 3, days: 12, … }
380
+
381
+ age.years; // 28
382
+ age.forHumans(); // '28 years'
383
+
384
+ // largestUnit controls the highest denomination in the result
385
+ post.createdAt.diffAsCarbonInterval(Carbon.now(), "day");
386
+ // → CarbonInterval { days: 165, hours: 3, minutes: 22, … }
387
+ ```
388
+
389
+ The `largestUnit` defaults to `'day'` and accepts `'year'`, `'month'`, `'week'`,
390
+ `'day'`, `'hour'`, `'minute'`, `'second'`, or `'millisecond'`.
391
+
392
+ ### diffForHumans
393
+
394
+ Returns a human-readable relative string using `Intl.RelativeTimeFormat`. With no
395
+ argument it compares against now.
396
+
397
+ ```typescript
398
+ // anywhere in your app
399
+ const posted = new Carbon("2026-06-08");
400
+ posted.diffForHumans(); // '1 week ago'
401
+
402
+ // Compare to a specific date instead of now
403
+ const a = new Carbon("2026-01-01");
404
+ const b = new Carbon("2026-06-09");
405
+ a.diffForHumans(b); // '5 months ago'
406
+ ```
407
+
408
+ Pass an options object to tune the output:
409
+
410
+ ```typescript
411
+ // anywhere in your app
412
+ a.diffForHumans(b, {
413
+ parts: 2, // include up to 2 units → '5 months, 1 week ago'
414
+ absolute: true, // drop "ago / from now" → '5 months, 1 week'
415
+ join: " and ", // custom joiner → '5 months and 1 week ago'
416
+ locale: "fr", // locale → 'il y a 5 mois'
417
+ intl: { numeric: "always" },
418
+ syntax: "ago", // force past phrasing ('ago' | 'from')
419
+ });
420
+ ```
421
+
422
+ ```typescript
423
+ // anywhere in your app
424
+ const future = Carbon.now().addDays(3);
425
+ future.diffForHumans(); // '3 days from now'
426
+ future.diffForHumans({ syntax: "ago" }); // '3 days ago'
427
+ future.diffForHumans({ absolute: true }); // '3 days'
428
+ ```
429
+
430
+ > **Tip** — `parts` defaults to `1`, so only the largest unit shows by default.
431
+ > Bump it to surface finer detail (`'5 months, 1 week ago'`).
432
+
433
+ ## Formatting
434
+
435
+ ### Token-based
436
+
437
+ ```typescript
438
+ // anywhere in your app
439
+ d.format(); // '2026-06-15 09:30:00' (default)
440
+ d.format("YYYY-MM-DD"); // '2026-06-15'
441
+ d.format("DD/MM/YYYY"); // '15/06/2026'
442
+ d.format("DDDD, DD MMMM YYYY"); // 'Monday, 15 June 2026'
443
+ d.format("HH:mm:ss"); // '09:30:00'
444
+ d.format("SSS"); // '042' (milliseconds)
445
+ d.format("YYYY-MM-DDTHH:mm:ssZ"); // '2026-06-15T09:30:00+02:00'
446
+ ```
447
+
448
+ | Token | Output | Example |
449
+ | ------ | ----------------------- | --------------- |
450
+ | `YYYY` | 4-digit year | `2026` |
451
+ | `YY` | 2-digit year | `26` |
452
+ | `MMMM` | Full month | `June` |
453
+ | `MMM` | Short month | `Jun` |
454
+ | `MM` | Month (padded) | `06` |
455
+ | `M` | Month | `6` |
456
+ | `DDDD` | Full weekday | `Monday` |
457
+ | `DDD` | Short weekday | `Mon` |
458
+ | `DD` | Day (padded) | `05` |
459
+ | `D` | Day | `5` |
460
+ | `HH` | Hour 24h (padded) | `09` |
461
+ | `H` | Hour 24h | `9` |
462
+ | `mm` | Minutes (padded) | `04` |
463
+ | `m` | Minutes | `4` |
464
+ | `ss` | Seconds (padded) | `07` |
465
+ | `s` | Seconds | `7` |
466
+ | `SSS` | Milliseconds | `042` |
467
+ | `SS` | Milliseconds (2 digits) | `04` |
468
+ | `S` | Milliseconds (1 digit) | `0` |
469
+ | `Z` | UTC offset | `+05:30` or `Z` |
470
+
471
+ ### Locale-aware
472
+
473
+ `intlFormat` delegates to `Intl.DateTimeFormat`:
474
+
475
+ ```typescript
476
+ // anywhere in your app
477
+ d.intlFormat("en-US", { dateStyle: "full" });
478
+ // → 'Monday, June 15, 2026'
479
+
480
+ d.intlFormat("fr-FR", { dateStyle: "long" });
481
+ // → '15 juin 2026'
482
+
483
+ d.intlFormat("en-ZA", { dateStyle: "short", timeStyle: "short" });
484
+ // → '2026/06/15, 09:30'
485
+
486
+ d.intlFormat("ja-JP", { year: "numeric", month: "long", day: "numeric" });
487
+ // → '2026年6月15日'
488
+ ```
489
+
490
+ ### Convenience formatters
491
+
492
+ ```typescript
493
+ // anywhere in your app
494
+ d.toDateString(); // '2026-06-15'
495
+ d.toDateTimeString(); // '2026-06-15 09:30:00'
496
+ d.toTimeString(); // '09:30:00'
497
+ d.toShortDate(); // 'Jun 15, 2026'
498
+ d.toLongDate(); // '15 June 2026'
499
+ ```
500
+
501
+ ### Which formatter should I use?
502
+
503
+ - **`format(token)`** — fixed, machine-style output you control exactly (logs,
504
+ filenames, API payloads). Locale-independent.
505
+ - **`intlFormat(locale, options)`** — output shown to a user whose locale and date
506
+ style should adapt. Delegates to the platform.
507
+ - **`toShortDate()` / `toLongDate()` / `toDateString()` etc.** — quick presets when
508
+ you don't want to remember tokens.
509
+
510
+ ## Serialisation
511
+
512
+ ```typescript
513
+ // anywhere in your app
514
+ d.toDate(); // native Date
515
+ d.toISOString(); // '2026-06-15T07:30:00+00:00' (UTC)
516
+ d.toDatabase(); // same — compatible with DB datetime columns
517
+ d.toUnix(); // 1_750_067_400 (seconds)
518
+ d.toMilliseconds(); // 1_750_067_400_000
519
+ d.valueOf(); // same as toMilliseconds() — enables < > comparisons
520
+ d.toJSON(); // same as toISOString() — used by JSON.stringify()
521
+ d.toString(); // '2026-06-15 09:30:00'
522
+
523
+ // Temporal interop
524
+ d.toZonedDateTime(); // Temporal.ZonedDateTime
525
+ d.toInstant(); // Temporal.Instant
526
+ d.toPlainDateTime(); // Temporal.PlainDateTime (loses timezone)
527
+ d.toPlainDate(); // Temporal.PlainDate (loses time + timezone)
528
+ ```
529
+
530
+ `valueOf()` enables direct comparison with `<`, `>`, `-`:
531
+
532
+ ```typescript
533
+ // anywhere in your app
534
+ const a = new Carbon("2026-01-01");
535
+ const b = new Carbon("2026-06-15");
536
+
537
+ a < b; // true
538
+ b - a; // ms between them
539
+ Math.min(+a, +b) === +a; // true
540
+ ```
541
+
542
+ ## CarbonInterval
543
+
544
+ `CarbonInterval` is an immutable duration value object backed by
545
+ `Temporal.Duration`. Every method returns a new instance.
546
+
547
+ ```typescript
548
+ // anywhere in your app
549
+ import { CarbonInterval } from "zerotal/carbon";
550
+
551
+ // Single-unit factories
552
+ const a = CarbonInterval.years(1);
553
+ const b = CarbonInterval.months(6);
554
+ const c = CarbonInterval.weeks(2);
555
+ const e = CarbonInterval.days(3);
556
+ const f = CarbonInterval.hours(4);
557
+ const g = CarbonInterval.minutes(30);
558
+ const h = CarbonInterval.seconds(90);
559
+ const i = CarbonInterval.milliseconds(500);
560
+ const j = CarbonInterval.microseconds(250);
561
+ const k = CarbonInterval.nanoseconds(100);
562
+
563
+ // From ISO 8601 duration string
564
+ const fromIso = CarbonInterval.fromISO("P1Y2M3DT4H5M6S");
565
+ const halfHour = CarbonInterval.fromISO("PT30M");
566
+
567
+ // From a Temporal.Duration
568
+ const fromDur = CarbonInterval.fromDuration(Temporal.Duration.from("P1D"));
569
+
570
+ // Direct constructor
571
+ const built = new CarbonInterval({ years: 1, months: 6, days: 3 });
572
+ ```
573
+
574
+ ### Fluent builder
575
+
576
+ Chain `and*` methods to compose multi-unit intervals:
577
+
578
+ ```typescript
579
+ // anywhere in your app
580
+ CarbonInterval.days(3).andHours(6).andMinutes(30);
581
+ // → 3 days 6 hours 30 minutes
582
+
583
+ CarbonInterval.years(1).andMonths(6);
584
+ // → 1 year 6 months
585
+
586
+ CarbonInterval.hours(2).andSeconds(45);
587
+ // → 2 hours 45 seconds
588
+ ```
589
+
590
+ Available: `andYears`, `andMonths`, `andWeeks`, `andDays`, `andHours`,
591
+ `andMinutes`, `andSeconds`, `andMilliseconds`, `andMicroseconds`, `andNanoseconds`.
592
+
593
+ ### Interval getters
594
+
595
+ ```typescript
596
+ // anywhere in your app
597
+ const i = CarbonInterval.fromISO("P1Y2M3DT4H5M6S");
598
+
599
+ i.years; // 1
600
+ i.months; // 2
601
+ i.weeks; // 0
602
+ i.days; // 3
603
+ i.hours; // 4
604
+ i.minutes; // 5
605
+ i.seconds; // 6
606
+ i.milliseconds; // 0
607
+ i.microseconds; // 0
608
+ i.nanoseconds; // 0
609
+
610
+ i.sign; // 1 (positive), -1 (negative), or 0 (zero)
611
+ i.isZero; // false
612
+ ```
613
+
614
+ ### Interval arithmetic
615
+
616
+ ```typescript
617
+ // anywhere in your app
618
+ const a = CarbonInterval.hours(2);
619
+ const b = CarbonInterval.minutes(30);
620
+
621
+ a.add(b); // 2 hours 30 minutes
622
+ a.subtract(b); // 1 hour 30 minutes
623
+ a.multiply(3); // 6 hours
624
+ a.negate(); // -2 hours
625
+ CarbonInterval.abs(a.negate()); // 2 hours (all fields positive)
626
+ ```
627
+
628
+ ### Normalization
629
+
630
+ `cascade()` rolls excess sub-units up into higher ones. It needs a reference date
631
+ for calendar-aware units (months, years) and defaults to now in UTC.
632
+
633
+ ```typescript
634
+ // anywhere in your app
635
+ CarbonInterval.seconds(90).cascade();
636
+ // → { minutes: 1, seconds: 30 }
637
+
638
+ CarbonInterval.minutes(90).cascade();
639
+ // → { hours: 1, minutes: 30 }
640
+
641
+ CarbonInterval.days(32).cascade();
642
+ // → { months: 1, days: 1 } (calendar-aware — exact result depends on reference month)
643
+
644
+ // Pass an explicit reference date
645
+ CarbonInterval.days(32).cascade(Carbon.today().inTimezone("UTC").toZonedDateTime());
646
+ ```
647
+
648
+ > **Warning** — Because `cascade()` is calendar-aware, the result of normalizing
649
+ > days into months depends on the reference month's length. Pass an explicit
650
+ > `relativeTo` when you need a deterministic outcome.
651
+
652
+ ### Total values
653
+
654
+ Calendar units (years, months) are approximated as average lengths.
655
+
656
+ ```typescript
657
+ // anywhere in your app
658
+ CarbonInterval.hours(2).andMinutes(30).totalMinutes(); // 150
659
+ CarbonInterval.days(3).andHours(6).totalHours(); // 78
660
+
661
+ i.totalSeconds(); // all fields converted to seconds
662
+ i.totalMinutes();
663
+ i.totalHours();
664
+ i.totalDays();
665
+ i.totalWeeks();
666
+ ```
667
+
668
+ ### Interval comparison
669
+
670
+ ```typescript
671
+ // anywhere in your app
672
+ const a = CarbonInterval.hours(2);
673
+ const b = CarbonInterval.minutes(90);
674
+
675
+ a.isGreaterThan(b); // true (2h > 1.5h)
676
+ b.isLessThan(a); // true
677
+ a.isEqualTo(b); // false
678
+
679
+ CarbonInterval.compare(a, b); // 1 (a > b), -1 (a < b), 0 (equal)
680
+ ```
681
+
682
+ ### Human-readable output
683
+
684
+ ```typescript
685
+ // anywhere in your app
686
+ CarbonInterval.days(1).andHours(2).andMinutes(30).forHumans();
687
+ // → '1 day 2 hours 30 minutes'
688
+
689
+ CarbonInterval.years(2).andMonths(3).forHumans({ join: " and " });
690
+ // → '2 years and 3 months'
691
+
692
+ CarbonInterval.hours(3).forHumans({ short: true });
693
+ // → '3 hou' (first 3 chars of each unit label)
694
+
695
+ i.toString(); // alias for forHumans()
696
+ ```
697
+
698
+ ### Interval serialisation
699
+
700
+ ```typescript
701
+ // anywhere in your app
702
+ CarbonInterval.days(1).andHours(2).toISO();
703
+ // → 'P1DT2H'
704
+
705
+ CarbonInterval.years(1).andMonths(6).andDays(3).toISO();
706
+ // → 'P1Y6M3D'
707
+
708
+ i.toJSON(); // same as toISO() — used by JSON.stringify()
709
+ i.toDuration(); // Temporal.Duration
710
+ ```
711
+
712
+ ### Using CarbonInterval with Carbon
713
+
714
+ An interval is what [`add()` and `subtract()`](#add-and-subtract) accept, and what
715
+ [`diffAsCarbonInterval()`](#differences) returns — the two directions between the
716
+ types:
717
+
718
+ ```typescript
719
+ // anywhere in your app
720
+ const interval = CarbonInterval.days(3).andHours(6);
721
+
722
+ Carbon.now().add(interval); // Carbon + interval → Carbon
723
+ birthday.diffAsCarbonInterval(Carbon.now(), "year"); // Carbon − Carbon → interval
724
+ ```
725
+
726
+ ## Testing
727
+
728
+ Set your suite up once as described in [Testing](/docs/testing). Carbon is
729
+ immutable and pure, so it needs no application — but a test that reaches for
730
+ `Carbon.now()` is a test that will fail on a Tuesday.
731
+
732
+ **Pin the instant.** Pass a fixed input rather than using the current time, and
733
+ the assertion holds forever:
734
+
735
+ ```typescript
736
+ // tests/dates/BillingPeriod.test.ts
737
+ import { test, expect } from "bun:test";
738
+ import { Carbon } from "zerotal/carbon";
739
+ import { periodFor } from "../../app/services/billing.ts";
740
+
741
+ test("a mid-month signup bills to the end of the month", () => {
742
+ const signedUp = Carbon.create("2026-03-14T09:00:00Z");
743
+
744
+ const period = periodFor(signedUp);
745
+
746
+ expect(period.end.toISOString()).toBe("2026-03-31T23:59:59.999Z");
747
+ });
748
+ ```
749
+
750
+ **Take the clock as an argument** in any code you want to test. A service that
751
+ calls `Carbon.now()` internally can only be tested by waiting or by mocking; one
752
+ that accepts a `now` parameter is tested by passing a date:
753
+
754
+ ```typescript
755
+ // app/services/billing.ts
756
+ export function periodFor(signedUp: Carbon, now: Carbon = Carbon.now()): Period {
757
+ // …
758
+ }
759
+ ```
760
+
761
+ That default keeps the call site clean while leaving the seam open — and it is
762
+ the difference between a test suite that is deterministic and one that fails at
763
+ month end.
764
+
765
+ **Test the boundaries you actually cross.** Month ends, leap days, and DST
766
+ transitions are where date code breaks, and none of them appear in a test written
767
+ around today:
768
+
769
+ ```typescript
770
+ // tests/dates/BillingPeriod.test.ts
771
+ test("handles a leap day", () => {
772
+ expect(Carbon.create("2028-02-29T12:00:00Z").addYears(1).toDateString()).toBe("2029-02-28");
773
+ });
774
+
775
+ test("survives a DST spring-forward", () => {
776
+ const before = Carbon.create("2026-03-29T00:30:00Z", "Europe/London");
777
+
778
+ expect(before.addHours(1).hour).toBe(2); // 01:30 does not exist locally
779
+ });
780
+ ```
781
+
782
+ ## References
783
+
784
+ ### Carbon — static factories
785
+
786
+ | Method | Signature | Description |
787
+ | ------------------ | --------------------------------------------------------- | ------------------------------------------------ |
788
+ | `now` | `now(timezone?: string): Carbon` | Current instant in the system or given timezone. |
789
+ | `create` | `create(input?: CarbonInput, timezone?: string): Carbon` | Parse/wrap any supported input (same as `new`). |
790
+ | `today` | `today(timezone?: string): Carbon` | Today at 00:00:00. |
791
+ | `tomorrow` | `tomorrow(timezone?: string): Carbon` | Tomorrow at 00:00:00. |
792
+ | `yesterday` | `yesterday(timezone?: string): Carbon` | Yesterday at 00:00:00. |
793
+ | `fromTimestamp` | `fromTimestamp(ts: number, timezone?: string): Carbon` | From a Unix timestamp in **seconds**. |
794
+ | `fromMilliseconds` | `fromMilliseconds(ms: number, timezone?: string): Carbon` | From a Unix timestamp in **milliseconds**. |
795
+
796
+ ### Carbon — selected instance methods
797
+
798
+ | Method | Signature | Description |
799
+ | ---------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------- |
800
+ | `inTimezone` | `inTimezone(tz: string): Carbon` | Same instant in a different timezone. |
801
+ | `add` / `subtract` | `add(interval: CarbonInterval): Carbon` | Apply a `CarbonInterval`. |
802
+ | `diffInMilliseconds` | `diffInMilliseconds(other: Carbon): number` | Raw millisecond difference `this - other`. |
803
+ | `diffAsCarbonInterval` | `diffAsCarbonInterval(other: Carbon, largestUnit?: DateTimeUnit): CarbonInterval` | Calendar-aware difference as an interval. |
804
+ | `diffForHumans` | `diffForHumans(other?: Carbon \| Date \| string, options?: DiffForHumansOptions): string` | Relative string via `Intl.RelativeTimeFormat`. |
805
+ | `format` | `format(template?: string): string` | Token-based formatting (default `YYYY-MM-DD HH:mm:ss`). |
806
+ | `intlFormat` | `intlFormat(locale?: string, options?: Intl.DateTimeFormatOptions): string` | Locale-aware formatting via `Intl.DateTimeFormat`. |
807
+ | `valueOf` | `valueOf(): number` | Epoch milliseconds — enables `<`, `>`, `-`. |
808
+ | `toDatabase` | `toDatabase(): string` | ISO string for DB datetime columns. |
809
+
810
+ ### CarbonInterval — members
811
+
812
+ | Method | Signature | Description |
813
+ | ------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------- |
814
+ | `years` … `nanoseconds` | `static years(amount: number): CarbonInterval` | Single-unit factories. |
815
+ | `fromISO` | `static fromISO(iso: string): CarbonInterval` | Parse an ISO 8601 duration string. |
816
+ | `fromDuration` | `static fromDuration(d: Temporal.Duration): CarbonInterval` | Wrap a `Temporal.Duration`. |
817
+ | `andYears` … `andNanoseconds` | `andHours(amount: number): CarbonInterval` | Fluent builder — add another unit. |
818
+ | `add` / `subtract` / `multiply` | `add(other: CarbonInterval): CarbonInterval` | Interval arithmetic. |
819
+ | `negate` / `abs` | `negate(): CarbonInterval` | Negate, or take the absolute value. |
820
+ | `cascade` | `cascade(relativeTo?: Temporal.ZonedDateTime): CarbonInterval` | Normalize sub-units up into higher units. |
821
+ | `totalSeconds` … `totalWeeks` | `totalHours(): number` | Total in a single unit (calendar units approximated). |
822
+ | `compare` | `static compare(a, b): -1 \| 0 \| 1` | Compare two intervals by total seconds. |
823
+ | `forHumans` | `forHumans(options?: { join?: string; short?: boolean }): string` | Human-readable description. |
824
+ | `toISO` | `toISO(): string` | ISO 8601 duration string (`toJSON` alias). |
825
+
826
+ ## Next steps
827
+
828
+ - [Casts & Mutators](/docs/orm/casts) — `datetime` columns hydrate to Carbon automatically.
829
+ - [Migrations](/docs/migrations) — `dateTime`, `timestamp`, and `date` column types.
830
+ - [Helpers](/docs/helpers) — other framework value objects and utilities.