@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- 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.
|