@zerotal/core 1.0.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 (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. package/src/view/jsx-runtime.ts +233 -0
@@ -0,0 +1,1396 @@
1
+ /**
2
+ * `Carbon` — an immutable date-time value object backed by
3
+ * `Temporal.ZonedDateTime`. Every modifier returns a new instance; the original
4
+ * is never mutated. Defaults to the system local timezone unless overridden, and
5
+ * formats via `Intl.DateTimeFormat` with no external dependencies.
6
+ */
7
+
8
+ import { Temporal } from "./temporal-shim.ts";
9
+ import { CarbonInterval } from "./CarbonInterval.ts";
10
+ import { _nowIn, _setTestInstant, _getTestInstant } from "./clock.ts";
11
+
12
+ // ── Types ─────────────────────────────────────────────────────────────────────
13
+
14
+ /** Any value `Carbon` knows how to parse or wrap into a date-time. */
15
+ export type CarbonInput =
16
+ | string
17
+ | number
18
+ | Date
19
+ | Carbon
20
+ | Temporal.ZonedDateTime
21
+ | Temporal.Instant
22
+ | Temporal.PlainDateTime
23
+ | Temporal.PlainDate;
24
+
25
+ /** How far {@link Carbon.travel} moves the test clock. Negative values go back. */
26
+ export interface TravelAmount {
27
+ years?: number;
28
+ months?: number;
29
+ weeks?: number;
30
+ days?: number;
31
+ hours?: number;
32
+ minutes?: number;
33
+ seconds?: number;
34
+ milliseconds?: number;
35
+ }
36
+
37
+ interface DiffForHumansOptions {
38
+ /** Which direction to phrase. Default: auto-detected from sign. */
39
+ syntax?: "ago" | "from";
40
+ /** Maximum number of time-unit parts to include. Default: 1 */
41
+ parts?: number;
42
+ /** Omit the ago/from-now suffix. Default: false */
43
+ absolute?: boolean;
44
+ /** Joiner between parts. Default: ', ' */
45
+ join?: string;
46
+ /** Locale for Intl.RelativeTimeFormat. Default: 'en' */
47
+ locale?: string;
48
+ /** Extra Intl options. Default: { numeric: 'auto' } */
49
+ intl?: Intl.RelativeTimeFormatOptions;
50
+ }
51
+
52
+ // ── Parse helpers ─────────────────────────────────────────────────────────────
53
+
54
+ function systemTz(): string {
55
+ return Temporal.Now.timeZoneId();
56
+ }
57
+
58
+ function toZdt(input: CarbonInput, tz: string): Temporal.ZonedDateTime {
59
+ if (input instanceof Carbon) return input._zdt;
60
+
61
+ if (input instanceof Temporal.ZonedDateTime) return input;
62
+
63
+ if (input instanceof Temporal.Instant) {
64
+ return input.toZonedDateTimeISO(tz);
65
+ }
66
+
67
+ if (input instanceof Temporal.PlainDateTime) {
68
+ return input.toZonedDateTime(tz);
69
+ }
70
+
71
+ if (input instanceof Temporal.PlainDate) {
72
+ return input.toZonedDateTime({ timeZone: tz, plainTime: Temporal.PlainTime.from("00:00:00") });
73
+ }
74
+
75
+ if (input instanceof Date) {
76
+ return Temporal.Instant.fromEpochMilliseconds(input.valueOf()).toZonedDateTimeISO(tz);
77
+ }
78
+
79
+ if (typeof input === "number") {
80
+ return Temporal.Instant.fromEpochMilliseconds(input).toZonedDateTimeISO(tz);
81
+ }
82
+
83
+ // string — try Temporal parsers in order of specificity
84
+ const text = input.trim();
85
+ try {
86
+ // Full ZonedDateTime with bracket timezone e.g. "2024-01-01T12:00:00+01:00[Europe/Paris]"
87
+ return Temporal.ZonedDateTime.from(text);
88
+ } catch {
89
+ /* fall through */
90
+ }
91
+ try {
92
+ // Instant (with Z or numeric offset) e.g. "2024-01-01T12:00:00Z"
93
+ return Temporal.Instant.from(text).toZonedDateTimeISO(tz);
94
+ } catch {
95
+ /* fall through */
96
+ }
97
+ try {
98
+ // PlainDateTime (no offset) e.g. "2024-01-01T12:00:00" or "2024-01-01 12:00:00"
99
+ const normalised = text.replace(" ", "T");
100
+ return Temporal.PlainDateTime.from(normalised).toZonedDateTime(tz);
101
+ } catch {
102
+ /* fall through */
103
+ }
104
+ try {
105
+ // PlainDate e.g. "2024-01-01"
106
+ return Temporal.PlainDate.from(text).toZonedDateTime({
107
+ timeZone: tz,
108
+ plainTime: Temporal.PlainTime.from("00:00:00"),
109
+ });
110
+ } catch {
111
+ /* fall through */
112
+ }
113
+
114
+ // Last resort: native Date parse (handles locale strings, RFC 2822, etc.)
115
+ const native = new Date(text);
116
+ if (!isNaN(native.valueOf())) {
117
+ return Temporal.Instant.fromEpochMilliseconds(native.valueOf()).toZonedDateTimeISO(tz);
118
+ }
119
+
120
+ throw new RangeError(`[Carbon] Cannot parse date: "${input}"`);
121
+ }
122
+
123
+ // ── Carbon ────────────────────────────────────────────────────────────────────
124
+
125
+ /**
126
+ * An immutable date-time value backed by `Temporal.ZonedDateTime`. Every
127
+ * modifier returns a new instance — the original is never mutated. Defaults to
128
+ * the system local timezone unless overridden, and formats via
129
+ * `Intl.DateTimeFormat`/token templates with no external dependencies.
130
+ *
131
+ * @example Construction and formatting
132
+ * ```ts
133
+ * import { Carbon } from "@zerotal/core/carbon";
134
+ *
135
+ * Carbon.now(); // current instant, system tz
136
+ * Carbon.create("2024-01-01T12:00:00Z"); // parse any supported input
137
+ * Carbon.now().format("YYYY-MM-DD HH:mm"); // "2024-06-09 14:30"
138
+ * ```
139
+ *
140
+ * @example Arithmetic, comparison and human diffs
141
+ * ```ts
142
+ * const start = Carbon.now();
143
+ * const later = start.addDays(3).subtractHours(2);
144
+ *
145
+ * later.isAfter(start); // true
146
+ * later.diffInHours(start); // 70
147
+ * later.diffForHumans(start); // "in 2 days"
148
+ * later.inTimezone("Europe/Paris"); // same instant, different zone
149
+ * ```
150
+ */
151
+ export class Carbon {
152
+ /** @internal — the backing Temporal.ZonedDateTime (treat as immutable) */
153
+ readonly _zdt: Temporal.ZonedDateTime;
154
+
155
+ /**
156
+ * Construct a `Carbon` from any supported input.
157
+ *
158
+ * @param input - Value to parse or wrap; defaults to the current instant.
159
+ * @param timezone - Timezone to interpret the value in; inferred from the
160
+ * input's own zone or the system zone when omitted.
161
+ * @throws {RangeError} When a string input cannot be parsed.
162
+ * @category Construction
163
+ */
164
+ constructor(input: CarbonInput = new Date(), timezone?: string) {
165
+ const tz =
166
+ timezone ??
167
+ (input instanceof Temporal.ZonedDateTime
168
+ ? input.timeZoneId
169
+ : input instanceof Carbon
170
+ ? input._zdt.timeZoneId
171
+ : systemTz());
172
+ this._zdt = toZdt(input, tz);
173
+ }
174
+
175
+ // ── Private helpers ───────────────────────────────────────────────────────
176
+
177
+ private _wrap(zdt: Temporal.ZonedDateTime): Carbon {
178
+ // Bypass the constructor's parsing by assigning the ZonedDateTime directly.
179
+ const wrapped = Object.create(Carbon.prototype) as Carbon;
180
+ (wrapped as unknown as { _zdt: Temporal.ZonedDateTime })._zdt = zdt;
181
+ return wrapped;
182
+ }
183
+
184
+ // ── Static factories ──────────────────────────────────────────────────────
185
+
186
+ /**
187
+ * Current date-time in the system (or given) timezone.
188
+ * @category Construction
189
+ */
190
+ static now(timezone?: string): Carbon {
191
+ const zdt = _nowIn(timezone ?? systemTz());
192
+ return new Carbon(zdt);
193
+ }
194
+
195
+ // ── Test clock ────────────────────────────────────────────────────────────
196
+
197
+ /**
198
+ * Freeze "now" at a fixed point, so behaviour that depends on the passage of
199
+ * time can be tested instead of waited out. Pass `null` to release it.
200
+ *
201
+ * Everything built on {@link Carbon.now} moves with it — `isPast`, `isToday`,
202
+ * `diffForHumans`, a model's timestamps. A raw `Date.now()` does not: this
203
+ * moves Carbon's clock, not the process's.
204
+ *
205
+ * Always release it, in an `afterEach`. A frozen clock that outlives its test
206
+ * makes the next one fail somewhere unrelated.
207
+ *
208
+ * @param value - The instant to freeze at, in any form Carbon parses.
209
+ * @category Testing
210
+ *
211
+ * @example
212
+ * Carbon.setTestNow('2025-01-01T00:00:00Z');
213
+ * expect(token.isExpired()).toBe(false);
214
+ * Carbon.setTestNow(null);
215
+ */
216
+ static setTestNow(value: CarbonInput | null): void {
217
+ _setTestInstant(value === null ? null : Carbon.create(value)._zdt.toInstant());
218
+ }
219
+
220
+ /**
221
+ * Freeze the clock — at `value` when given, otherwise at the current instant.
222
+ * Returns the frozen `Carbon` so the test can assert against it.
223
+ *
224
+ * @category Testing
225
+ *
226
+ * @example
227
+ * const start = Carbon.freeze();
228
+ * await service.run();
229
+ * expect(job.startedAt.equalTo(start)).toBe(true);
230
+ */
231
+ static freeze(value?: CarbonInput): Carbon {
232
+ Carbon.setTestNow(value ?? Carbon.now());
233
+ return Carbon.now();
234
+ }
235
+
236
+ /**
237
+ * Jump the clock to an absolute point. Identical to {@link Carbon.setTestNow}
238
+ * with a value, and reads better at a call site that is moving through time.
239
+ *
240
+ * @category Testing
241
+ *
242
+ * @example
243
+ * Carbon.travelTo('2026-01-01');
244
+ */
245
+ static travelTo(value: CarbonInput): Carbon {
246
+ Carbon.setTestNow(value);
247
+ return Carbon.now();
248
+ }
249
+
250
+ /**
251
+ * Move the clock relative to where it is now, freezing it if it was running.
252
+ *
253
+ * @param amount - Units to move by; negative values go backwards.
254
+ * @category Testing
255
+ *
256
+ * @example
257
+ * Carbon.freeze();
258
+ * Carbon.travel({ days: 8 });
259
+ * expect(invitation.isExpired()).toBe(true);
260
+ */
261
+ static travel(amount: TravelAmount): Carbon {
262
+ const moved = Carbon.create(Carbon.now()._zdt.add(amount));
263
+ Carbon.setTestNow(moved);
264
+ return moved;
265
+ }
266
+
267
+ /** Let the clock run normally again. Call it in `afterEach`. @category Testing */
268
+ static release(): void {
269
+ _setTestInstant(null);
270
+ }
271
+
272
+ /** Whether the clock is currently frozen. @category Testing */
273
+ static isFrozen(): boolean {
274
+ return _getTestInstant() !== null;
275
+ }
276
+
277
+ /**
278
+ * Freeze the clock for the duration of `fn`, then release it — whether `fn`
279
+ * returns or throws. The scoped form, for when a test must not leak a frozen
280
+ * clock into the next one.
281
+ *
282
+ * @category Testing
283
+ *
284
+ * @example
285
+ * await Carbon.withTestNow('2025-06-01', async () => {
286
+ * await service.expireStaleCarts();
287
+ * });
288
+ */
289
+ static async withTestNow<T>(value: CarbonInput, fn: () => T | Promise<T>): Promise<T> {
290
+ const previous = _getTestInstant();
291
+ Carbon.setTestNow(value);
292
+ try {
293
+ return await fn();
294
+ } finally {
295
+ _setTestInstant(previous);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Parse / wrap any supported input into a `Carbon`.
301
+ *
302
+ * Accepts ISO 8601 strings (zoned, instant, plain date-time, or plain date),
303
+ * `"YYYY-MM-DD HH:mm:ss"` with a space separator, epoch numbers (ms), native
304
+ * `Date`, another `Carbon`, or a `Temporal` value; falls back to native
305
+ * `Date` parsing as a last resort.
306
+ *
307
+ * @param input - The value to parse or wrap. Defaults to now.
308
+ * @param timezone - Timezone to interpret the value in; defaults to the input's
309
+ * own zone (for zoned inputs) or the system zone.
310
+ * @returns A new `Carbon` instance.
311
+ * @throws {RangeError} When a string input cannot be parsed by any strategy.
312
+ * @category Construction
313
+ */
314
+ static create(input: CarbonInput = new Date(), timezone?: string): Carbon {
315
+ return new Carbon(input, timezone);
316
+ }
317
+
318
+ /**
319
+ * Today at midnight in the system timezone.
320
+ * @category Construction
321
+ */
322
+ static today(timezone?: string): Carbon {
323
+ return Carbon.now(timezone).startOfDay();
324
+ }
325
+
326
+ /**
327
+ * Tomorrow at midnight in the system (or given) timezone.
328
+ * @category Construction
329
+ */
330
+ static tomorrow(timezone?: string): Carbon {
331
+ return Carbon.now(timezone).addDays(1).startOfDay();
332
+ }
333
+
334
+ /**
335
+ * Yesterday at midnight in the system (or given) timezone.
336
+ * @category Construction
337
+ */
338
+ static yesterday(timezone?: string): Carbon {
339
+ return Carbon.now(timezone).subtractDays(1).startOfDay();
340
+ }
341
+
342
+ /**
343
+ * First moment of the current month.
344
+ * @category Construction
345
+ */
346
+ static startOfMonth(timezone?: string): Carbon {
347
+ return Carbon.now(timezone).startOfMonth();
348
+ }
349
+
350
+ /**
351
+ * Last moment of the current month.
352
+ * @category Construction
353
+ */
354
+ static endOfMonth(timezone?: string): Carbon {
355
+ return Carbon.now(timezone).endOfMonth();
356
+ }
357
+
358
+ /**
359
+ * First moment of the current ISO week (Monday 00:00).
360
+ * @category Construction
361
+ */
362
+ static startOfWeek(timezone?: string): Carbon {
363
+ return Carbon.now(timezone).startOfWeek();
364
+ }
365
+
366
+ /**
367
+ * Last moment of the current ISO week (Sunday 23:59:59.999).
368
+ * @category Construction
369
+ */
370
+ static endOfWeek(timezone?: string): Carbon {
371
+ return Carbon.now(timezone).endOfWeek();
372
+ }
373
+
374
+ /**
375
+ * First moment of the current year.
376
+ * @category Construction
377
+ */
378
+ static startOfYear(timezone?: string): Carbon {
379
+ return Carbon.now(timezone).startOfYear();
380
+ }
381
+
382
+ /**
383
+ * Last moment of the current year.
384
+ * @category Construction
385
+ */
386
+ static endOfYear(timezone?: string): Carbon {
387
+ return Carbon.now(timezone).endOfYear();
388
+ }
389
+
390
+ /**
391
+ * Create a Carbon from a Unix timestamp (seconds).
392
+ * @category Construction
393
+ */
394
+ static fromTimestamp(ts: number, timezone?: string): Carbon {
395
+ return new Carbon(Temporal.Instant.fromEpochMilliseconds(ts * 1000), timezone);
396
+ }
397
+
398
+ /**
399
+ * Create a Carbon from a Unix timestamp in milliseconds.
400
+ * @category Construction
401
+ */
402
+ static fromMilliseconds(ms: number, timezone?: string): Carbon {
403
+ return new Carbon(Temporal.Instant.fromEpochMilliseconds(ms), timezone);
404
+ }
405
+
406
+ // ── Timezone ──────────────────────────────────────────────────────────────
407
+
408
+ /**
409
+ * Return the timezone ID (e.g. 'America/New_York').
410
+ * @category Timezone
411
+ */
412
+ get timezone(): string {
413
+ return this._zdt.timeZoneId;
414
+ }
415
+
416
+ /**
417
+ * Return a new Carbon representing the same instant in a different timezone.
418
+ * @category Timezone
419
+ */
420
+ inTimezone(tz: string): Carbon {
421
+ return this._wrap(this._zdt.withTimeZone(tz));
422
+ }
423
+
424
+ // ── Getters ───────────────────────────────────────────────────────────────
425
+
426
+ /** Full year (e.g. 2024). @category Getters */
427
+ get year(): number {
428
+ return this._zdt.year;
429
+ }
430
+ /** Month of year, 1–12. @category Getters */
431
+ get month(): number {
432
+ return this._zdt.month;
433
+ } // 1–12
434
+ /** Day of month, 1–31. @category Getters */
435
+ get day(): number {
436
+ return this._zdt.day;
437
+ } // 1–31
438
+ /** Hour of day, 0–23. @category Getters */
439
+ get hour(): number {
440
+ return this._zdt.hour;
441
+ }
442
+ /** Minute, 0–59. @category Getters */
443
+ get minute(): number {
444
+ return this._zdt.minute;
445
+ }
446
+ /** Second, 0–59. @category Getters */
447
+ get second(): number {
448
+ return this._zdt.second;
449
+ }
450
+ /** Millisecond, 0–999. @category Getters */
451
+ get millisecond(): number {
452
+ return this._zdt.millisecond;
453
+ }
454
+ /** Microsecond component, 0–999. @category Getters */
455
+ get microsecond(): number {
456
+ return this._zdt.microsecond;
457
+ }
458
+ /** Nanosecond component, 0–999. @category Getters */
459
+ get nanosecond(): number {
460
+ return this._zdt.nanosecond;
461
+ }
462
+
463
+ /**
464
+ * Day of week: 1 = Monday … 7 = Sunday (ISO 8601).
465
+ * Note: differs from JS `Date.getDay()` which uses 0 = Sunday.
466
+ * @category Getters
467
+ */
468
+ get dayOfWeek(): number {
469
+ return this._zdt.dayOfWeek;
470
+ }
471
+
472
+ /**
473
+ * Day of year (1–366).
474
+ * @category Getters
475
+ */
476
+ get dayOfYear(): number {
477
+ return this._zdt.dayOfYear;
478
+ }
479
+
480
+ /**
481
+ * Week of year (ISO 8601).
482
+ * @category Getters
483
+ */
484
+ get weekOfYear(): number {
485
+ return this._zdt.weekOfYear ?? 1;
486
+ }
487
+
488
+ /**
489
+ * Full English month name (e.g. "January"), in **this instance's** timezone.
490
+ *
491
+ * `toDate()` yields a bare instant, and `Intl.DateTimeFormat` with no `timeZone` formats
492
+ * it in the *system* zone — so a Tokyo Carbon whose `.month` is 1 reported "December" on
493
+ * a machine running behind it. Every field getter on this class answers in the
494
+ * instance's zone; these have to agree with them.
495
+ *
496
+ * @category Getters
497
+ */
498
+ get monthName(): string {
499
+ return new Intl.DateTimeFormat("en", {
500
+ month: "long",
501
+ timeZone: this.timezone,
502
+ }).format(this.toDate());
503
+ }
504
+
505
+ /** Full English weekday name (e.g. "Monday"), in this instance's timezone. @category Getters */
506
+ get dayName(): string {
507
+ return new Intl.DateTimeFormat("en", {
508
+ weekday: "long",
509
+ timeZone: this.timezone,
510
+ }).format(this.toDate());
511
+ }
512
+
513
+ // ── Add ───────────────────────────────────────────────────────────────────
514
+
515
+ /** Return a copy advanced by `amount` nanoseconds. @category Arithmetic */
516
+ addNanoseconds(amount: number): Carbon {
517
+ return this._wrap(this._zdt.add({ nanoseconds: amount }));
518
+ }
519
+ /** Return a copy advanced by `amount` microseconds. @category Arithmetic */
520
+ addMicroseconds(amount: number): Carbon {
521
+ return this._wrap(this._zdt.add({ microseconds: amount }));
522
+ }
523
+ /** Return a copy advanced by `amount` milliseconds. @category Arithmetic */
524
+ addMilliseconds(amount: number): Carbon {
525
+ return this._wrap(this._zdt.add({ milliseconds: amount }));
526
+ }
527
+ /** Return a copy advanced by `amount` seconds. @category Arithmetic */
528
+ addSeconds(amount: number): Carbon {
529
+ return this._wrap(this._zdt.add({ seconds: amount }));
530
+ }
531
+ /** Return a copy advanced by `amount` minutes. @category Arithmetic */
532
+ addMinutes(amount: number): Carbon {
533
+ return this._wrap(this._zdt.add({ minutes: amount }));
534
+ }
535
+ /** Return a copy advanced by `amount` hours. @category Arithmetic */
536
+ addHours(amount: number): Carbon {
537
+ return this._wrap(this._zdt.add({ hours: amount }));
538
+ }
539
+ /** Return a copy advanced by `amount` days. @category Arithmetic */
540
+ addDays(amount: number): Carbon {
541
+ return this._wrap(this._zdt.add({ days: amount }));
542
+ }
543
+ /** Return a copy advanced by `amount` weeks. @category Arithmetic */
544
+ addWeeks(amount: number): Carbon {
545
+ return this._wrap(this._zdt.add({ weeks: amount }));
546
+ }
547
+ /** Return a copy advanced by `amount` calendar months. @category Arithmetic */
548
+ addMonths(amount: number): Carbon {
549
+ return this._wrap(this._zdt.add({ months: amount }));
550
+ }
551
+ /** Return a copy advanced by `amount` calendar years. @category Arithmetic */
552
+ addYears(amount: number): Carbon {
553
+ return this._wrap(this._zdt.add({ years: amount }));
554
+ }
555
+ /** Return a copy advanced by `amount` decades (10 years). @category Arithmetic */
556
+ addDecades(amount: number): Carbon {
557
+ return this.addYears(amount * 10);
558
+ }
559
+ /** Return a copy advanced by `amount` centuries (100 years). @category Arithmetic */
560
+ addCenturies(amount: number): Carbon {
561
+ return this.addYears(amount * 100);
562
+ }
563
+ /** Return a copy advanced by `amount` millennia (1000 years). @category Arithmetic */
564
+ addMillennia(amount: number): Carbon {
565
+ return this.addYears(amount * 1000);
566
+ }
567
+
568
+ // ── Subtract ──────────────────────────────────────────────────────────────
569
+
570
+ /** Return a copy moved back by `amount` nanoseconds. @category Arithmetic */
571
+ subtractNanoseconds(amount: number): Carbon {
572
+ return this.addNanoseconds(-amount);
573
+ }
574
+ /** Return a copy moved back by `amount` microseconds. @category Arithmetic */
575
+ subtractMicroseconds(amount: number): Carbon {
576
+ return this.addMicroseconds(-amount);
577
+ }
578
+ /** Return a copy moved back by `amount` milliseconds. @category Arithmetic */
579
+ subtractMilliseconds(amount: number): Carbon {
580
+ return this.addMilliseconds(-amount);
581
+ }
582
+ /** Return a copy moved back by `amount` seconds. @category Arithmetic */
583
+ subtractSeconds(amount: number): Carbon {
584
+ return this.addSeconds(-amount);
585
+ }
586
+ /** Return a copy moved back by `amount` minutes. @category Arithmetic */
587
+ subtractMinutes(amount: number): Carbon {
588
+ return this.addMinutes(-amount);
589
+ }
590
+ /** Return a copy moved back by `amount` hours. @category Arithmetic */
591
+ subtractHours(amount: number): Carbon {
592
+ return this.addHours(-amount);
593
+ }
594
+ /** Return a copy moved back by `amount` days. @category Arithmetic */
595
+ subtractDays(amount: number): Carbon {
596
+ return this.addDays(-amount);
597
+ }
598
+ /** Return a copy moved back by `amount` weeks. @category Arithmetic */
599
+ subtractWeeks(amount: number): Carbon {
600
+ return this.addWeeks(-amount);
601
+ }
602
+ /** Return a copy moved back by `amount` calendar months. @category Arithmetic */
603
+ subtractMonths(amount: number): Carbon {
604
+ return this.addMonths(-amount);
605
+ }
606
+ /** Return a copy moved back by `amount` calendar years. @category Arithmetic */
607
+ subtractYears(amount: number): Carbon {
608
+ return this.addYears(-amount);
609
+ }
610
+ /** Return a copy moved back by `amount` decades. @category Arithmetic */
611
+ subtractDecades(amount: number): Carbon {
612
+ return this.addDecades(-amount);
613
+ }
614
+ /** Return a copy moved back by `amount` centuries. @category Arithmetic */
615
+ subtractCenturies(amount: number): Carbon {
616
+ return this.addCenturies(-amount);
617
+ }
618
+ /** Return a copy moved back by `amount` millennia. @category Arithmetic */
619
+ subtractMillennia(amount: number): Carbon {
620
+ return this.addMillennia(-amount);
621
+ }
622
+
623
+ // sub* short aliases
624
+ /** Short alias of {@link subtractNanoseconds}. @category Arithmetic */
625
+ subNanoseconds(amount: number): Carbon {
626
+ return this.addNanoseconds(-amount);
627
+ }
628
+ /** Short alias of {@link subtractMicroseconds}. @category Arithmetic */
629
+ subMicroseconds(amount: number): Carbon {
630
+ return this.addMicroseconds(-amount);
631
+ }
632
+ /** Short alias of {@link subtractMilliseconds}. @category Arithmetic */
633
+ subMilliseconds(amount: number): Carbon {
634
+ return this.addMilliseconds(-amount);
635
+ }
636
+ /** Short alias of {@link subtractSeconds}. @category Arithmetic */
637
+ subSeconds(amount: number): Carbon {
638
+ return this.addSeconds(-amount);
639
+ }
640
+ /** Short alias of {@link subtractMinutes}. @category Arithmetic */
641
+ subMinutes(amount: number): Carbon {
642
+ return this.addMinutes(-amount);
643
+ }
644
+ /** Short alias of {@link subtractHours}. @category Arithmetic */
645
+ subHours(amount: number): Carbon {
646
+ return this.addHours(-amount);
647
+ }
648
+ /** Short alias of {@link subtractDays}. @category Arithmetic */
649
+ subDays(amount: number): Carbon {
650
+ return this.addDays(-amount);
651
+ }
652
+ /** Short alias of {@link subtractWeeks}. @category Arithmetic */
653
+ subWeeks(amount: number): Carbon {
654
+ return this.addWeeks(-amount);
655
+ }
656
+ /** Short alias of {@link subtractMonths}. @category Arithmetic */
657
+ subMonths(amount: number): Carbon {
658
+ return this.addMonths(-amount);
659
+ }
660
+ /** Short alias of {@link subtractYears}. @category Arithmetic */
661
+ subYears(amount: number): Carbon {
662
+ return this.addYears(-amount);
663
+ }
664
+ /** Short alias of {@link subtractDecades}. @category Arithmetic */
665
+ subDecades(amount: number): Carbon {
666
+ return this.addDecades(-amount);
667
+ }
668
+
669
+ // ── Apply a CarbonInterval ────────────────────────────────────────────────
670
+
671
+ /**
672
+ * Add a {@link CarbonInterval} to this instance.
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * Carbon.now().add(CarbonInterval.days(3).andHours(6));
677
+ * ```
678
+ * @category Arithmetic
679
+ */
680
+ add(interval: { _duration: Temporal.Duration }): Carbon {
681
+ return this._wrap(this._zdt.add(interval._duration));
682
+ }
683
+
684
+ /**
685
+ * Subtract a {@link CarbonInterval} from this instance.
686
+ *
687
+ * @example
688
+ * ```ts
689
+ * Carbon.now().subtract(CarbonInterval.weeks(1));
690
+ * ```
691
+ * @category Arithmetic
692
+ */
693
+ subtract(interval: { _duration: Temporal.Duration }): Carbon {
694
+ return this._wrap(this._zdt.subtract(interval._duration));
695
+ }
696
+
697
+ // ── Boundary setters (immutable) ──────────────────────────────────────────
698
+
699
+ /**
700
+ * Copy set to the first moment of the day (00:00:00.000).
701
+ *
702
+ * `disambiguation: "earlier"` matters on a fall-back day in a zone that repeats midnight
703
+ * (Santiago and Havana do). Temporal's default there is the *second* occurrence, so a
704
+ * `startOfDay()`…`endOfDay()` range silently began an hour late and lost every row in
705
+ * the first hour of the day.
706
+ *
707
+ * @category Boundaries
708
+ */
709
+ startOfDay(): Carbon {
710
+ return this._wrap(
711
+ this._zdt.with(
712
+ {
713
+ hour: 0,
714
+ minute: 0,
715
+ second: 0,
716
+ millisecond: 0,
717
+ microsecond: 0,
718
+ nanosecond: 0,
719
+ },
720
+ { disambiguation: "earlier" },
721
+ ),
722
+ );
723
+ }
724
+
725
+ /** Copy set to the last moment of the day (23:59:59.999999999). @category Boundaries */
726
+ endOfDay(): Carbon {
727
+ return this._wrap(
728
+ this._zdt.with({
729
+ hour: 23,
730
+ minute: 59,
731
+ second: 59,
732
+ millisecond: 999,
733
+ microsecond: 999,
734
+ nanosecond: 999,
735
+ }),
736
+ );
737
+ }
738
+
739
+ /** Copy set to the first moment of the hour. @category Boundaries */
740
+ startOfHour(): Carbon {
741
+ return this._wrap(
742
+ this._zdt.with({ minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }),
743
+ );
744
+ }
745
+
746
+ /** Copy set to the last moment of the hour. @category Boundaries */
747
+ endOfHour(): Carbon {
748
+ return this._wrap(
749
+ this._zdt.with({
750
+ minute: 59,
751
+ second: 59,
752
+ millisecond: 999,
753
+ microsecond: 999,
754
+ nanosecond: 999,
755
+ }),
756
+ );
757
+ }
758
+
759
+ /** Copy set to the first moment of the minute. @category Boundaries */
760
+ startOfMinute(): Carbon {
761
+ return this._wrap(this._zdt.with({ second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }));
762
+ }
763
+
764
+ /** Copy set to the last moment of the minute. @category Boundaries */
765
+ endOfMinute(): Carbon {
766
+ return this._wrap(
767
+ this._zdt.with({ second: 59, millisecond: 999, microsecond: 999, nanosecond: 999 }),
768
+ );
769
+ }
770
+
771
+ /** Copy set to the first moment of the first day of the month. @category Boundaries */
772
+ startOfMonth(): Carbon {
773
+ return this._wrap(
774
+ this._zdt.with({
775
+ day: 1,
776
+ hour: 0,
777
+ minute: 0,
778
+ second: 0,
779
+ millisecond: 0,
780
+ microsecond: 0,
781
+ nanosecond: 0,
782
+ }),
783
+ );
784
+ }
785
+
786
+ /** Copy set to the last moment of the last day of the month. @category Boundaries */
787
+ endOfMonth(): Carbon {
788
+ return this._wrap(
789
+ this._zdt.with({
790
+ day: this._zdt.daysInMonth,
791
+ hour: 23,
792
+ minute: 59,
793
+ second: 59,
794
+ millisecond: 999,
795
+ microsecond: 999,
796
+ nanosecond: 999,
797
+ }),
798
+ );
799
+ }
800
+
801
+ /**
802
+ * Start of ISO week (Monday 00:00:00).
803
+ * @category Boundaries
804
+ */
805
+ startOfWeek(): Carbon {
806
+ // dayOfWeek: 1=Mon … 7=Sun
807
+ const back = this._zdt.dayOfWeek - 1; // days back to Monday
808
+ return this.subtractDays(back).startOfDay();
809
+ }
810
+
811
+ /**
812
+ * End of ISO week (Sunday 23:59:59.999999999).
813
+ * @category Boundaries
814
+ */
815
+ endOfWeek(): Carbon {
816
+ return this.startOfWeek().addDays(6).endOfDay();
817
+ }
818
+
819
+ /** Copy set to the first moment of January 1st. @category Boundaries */
820
+ startOfYear(): Carbon {
821
+ return this._wrap(
822
+ this._zdt.with({
823
+ month: 1,
824
+ day: 1,
825
+ hour: 0,
826
+ minute: 0,
827
+ second: 0,
828
+ millisecond: 0,
829
+ microsecond: 0,
830
+ nanosecond: 0,
831
+ }),
832
+ );
833
+ }
834
+
835
+ /** Copy set to the last moment of December 31st. @category Boundaries */
836
+ endOfYear(): Carbon {
837
+ return this._wrap(
838
+ this._zdt.with({
839
+ month: 12,
840
+ day: 31,
841
+ hour: 23,
842
+ minute: 59,
843
+ second: 59,
844
+ millisecond: 999,
845
+ microsecond: 999,
846
+ nanosecond: 999,
847
+ }),
848
+ );
849
+ }
850
+
851
+ /** Copy set to the first moment of the decade (year ending in 0). @category Boundaries */
852
+ startOfDecade(): Carbon {
853
+ return this.withYear(Math.floor(this.year / 10) * 10).startOfYear();
854
+ }
855
+
856
+ /** Copy set to the last moment of the decade (year ending in 9). @category Boundaries */
857
+ endOfDecade(): Carbon {
858
+ return this.withYear(Math.floor(this.year / 10) * 10 + 9).endOfYear();
859
+ }
860
+
861
+ /** Copy set to the first moment of the century. @category Boundaries */
862
+ startOfCentury(): Carbon {
863
+ return this.withYear(Math.floor(this.year / 100) * 100).startOfYear();
864
+ }
865
+
866
+ /** Copy set to the last moment of the century. @category Boundaries */
867
+ endOfCentury(): Carbon {
868
+ return this.withYear(Math.floor(this.year / 100) * 100 + 99).endOfYear();
869
+ }
870
+
871
+ // ── Field setters (immutable — "with" prefix signals new instance) ─────────
872
+
873
+ /** Copy with the year replaced. @category Setters */
874
+ withYear(amount: number): Carbon {
875
+ return this._wrap(this._zdt.with({ year: amount }));
876
+ }
877
+ /** Copy with the month (1–12) replaced. @category Setters */
878
+ withMonth(amount: number): Carbon {
879
+ return this._wrap(this._zdt.with({ month: amount }));
880
+ }
881
+ /** Copy with the day of month replaced. @category Setters */
882
+ withDay(amount: number): Carbon {
883
+ return this._wrap(this._zdt.with({ day: amount }));
884
+ }
885
+ /** Copy with the hour (0–23) replaced. @category Setters */
886
+ withHour(amount: number): Carbon {
887
+ return this._wrap(this._zdt.with({ hour: amount }));
888
+ }
889
+ /** Copy with the minute replaced. @category Setters */
890
+ withMinute(amount: number): Carbon {
891
+ return this._wrap(this._zdt.with({ minute: amount }));
892
+ }
893
+ /** Copy with the second replaced. @category Setters */
894
+ withSecond(amount: number): Carbon {
895
+ return this._wrap(this._zdt.with({ second: amount }));
896
+ }
897
+ /** Copy with the millisecond replaced. @category Setters */
898
+ withMillisecond(amount: number): Carbon {
899
+ return this._wrap(this._zdt.with({ millisecond: amount }));
900
+ }
901
+ /** Copy with the microsecond replaced. @category Setters */
902
+ withMicrosecond(amount: number): Carbon {
903
+ return this._wrap(this._zdt.with({ microsecond: amount }));
904
+ }
905
+ /** Copy with the nanosecond replaced. @category Setters */
906
+ withNanosecond(amount: number): Carbon {
907
+ return this._wrap(this._zdt.with({ nanosecond: amount }));
908
+ }
909
+
910
+ /** Copy with the time-of-day replaced (microsecond/nanosecond zeroed). @category Setters */
911
+ withTime(hours: number, minutes: number, seconds = 0, ms = 0): Carbon {
912
+ return this._wrap(
913
+ this._zdt.with({
914
+ hour: hours,
915
+ minute: minutes,
916
+ second: seconds,
917
+ millisecond: ms,
918
+ microsecond: 0,
919
+ nanosecond: 0,
920
+ }),
921
+ );
922
+ }
923
+
924
+ // ── Predicates ────────────────────────────────────────────────────────────
925
+
926
+ /** Whether this date falls on the current calendar day. @category Comparison */
927
+ isToday(): boolean {
928
+ const now = _nowIn(this._zdt.timeZoneId);
929
+ return Temporal.PlainDate.compare(this._zdt.toPlainDate(), now.toPlainDate()) === 0;
930
+ }
931
+
932
+ /** Whether this date falls on tomorrow's calendar day. @category Comparison */
933
+ isTomorrow(): boolean {
934
+ const tom = _nowIn(this._zdt.timeZoneId).add({ days: 1 });
935
+ return Temporal.PlainDate.compare(this._zdt.toPlainDate(), tom.toPlainDate()) === 0;
936
+ }
937
+
938
+ /** Whether this date falls on yesterday's calendar day. @category Comparison */
939
+ isYesterday(): boolean {
940
+ const yes = _nowIn(this._zdt.timeZoneId).subtract({ days: 1 });
941
+ return Temporal.PlainDate.compare(this._zdt.toPlainDate(), yes.toPlainDate()) === 0;
942
+ }
943
+
944
+ /** Whether this instant is before now. @category Comparison */
945
+ isPast(): boolean {
946
+ return Temporal.ZonedDateTime.compare(this._zdt, _nowIn(this._zdt.timeZoneId)) < 0;
947
+ }
948
+ /** Whether this instant is after now. @category Comparison */
949
+ isFuture(): boolean {
950
+ return Temporal.ZonedDateTime.compare(this._zdt, _nowIn(this._zdt.timeZoneId)) > 0;
951
+ }
952
+
953
+ /**
954
+ * Weekend: Saturday (6) or Sunday (7) in ISO dayOfWeek.
955
+ * @category Comparison
956
+ */
957
+ isWeekend(): boolean {
958
+ return this._zdt.dayOfWeek >= 6;
959
+ }
960
+ /** Whether this date is Monday–Friday. @category Comparison */
961
+ isWeekday(): boolean {
962
+ return !this.isWeekend();
963
+ }
964
+
965
+ /** Whether this date's year is a leap year. @category Comparison */
966
+ isLeapYear(): boolean {
967
+ return this._zdt.inLeapYear;
968
+ }
969
+
970
+ /** Whether both fall on the same calendar day. @category Comparison */
971
+ isSameDay(other: Carbon): boolean {
972
+ return Temporal.PlainDate.compare(this._zdt.toPlainDate(), other._zdt.toPlainDate()) === 0;
973
+ }
974
+
975
+ /** Whether both fall in the same calendar month and year. @category Comparison */
976
+ isSameMonth(other: Carbon): boolean {
977
+ return this.year === other.year && this.month === other.month;
978
+ }
979
+
980
+ /** Whether both fall in the same calendar year. @category Comparison */
981
+ isSameYear(other: Carbon): boolean {
982
+ return this.year === other.year;
983
+ }
984
+
985
+ /** Whether this instant is strictly before `other`. @category Comparison */
986
+ isBefore(other: Carbon): boolean {
987
+ return Temporal.ZonedDateTime.compare(this._zdt, other._zdt) < 0;
988
+ }
989
+
990
+ /** Whether this instant is strictly after `other`. @category Comparison */
991
+ isAfter(other: Carbon): boolean {
992
+ return Temporal.ZonedDateTime.compare(this._zdt, other._zdt) > 0;
993
+ }
994
+
995
+ /** Whether this instant equals `other`. @category Comparison */
996
+ isEqual(other: Carbon): boolean {
997
+ return Temporal.ZonedDateTime.compare(this._zdt, other._zdt) === 0;
998
+ }
999
+
1000
+ /**
1001
+ * Whether this instant lies between `start` and `end`.
1002
+ *
1003
+ * @param inclusive - Include the endpoints when `true` (default).
1004
+ * @category Comparison
1005
+ */
1006
+ isBetween(start: Carbon, end: Carbon, inclusive = true): boolean {
1007
+ if (inclusive) return !this.isBefore(start) && !this.isAfter(end);
1008
+ return this.isAfter(start) && this.isBefore(end);
1009
+ }
1010
+
1011
+ // ── Calendar queries ──────────────────────────────────────────────────────
1012
+
1013
+ /** Number of days in this date's month (28–31). @category Getters */
1014
+ daysInMonth(): number {
1015
+ return this._zdt.daysInMonth;
1016
+ }
1017
+ /** Number of days in this date's year (365 or 366). @category Getters */
1018
+ daysInYear(): number {
1019
+ return this._zdt.daysInYear;
1020
+ }
1021
+ /** Number of ISO weeks in this date's year (52 or 53). @category Getters */
1022
+ weeksInYear(): number {
1023
+ // Temporal dropped `weeksInYear` from ZonedDateTime, so compute the ISO-8601
1024
+ // week count: a year has 53 weeks when it starts on a Thursday, or is a leap
1025
+ // year starting on a Wednesday; otherwise 52.
1026
+ const year = this._zdt.year;
1027
+ const startDayOfWeek = new Date(Date.UTC(year, 0, 1)).getUTCDay(); // 0=Sun … 6=Sat
1028
+ const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
1029
+ return startDayOfWeek === 4 || (isLeapYear && startDayOfWeek === 3) ? 53 : 52;
1030
+ }
1031
+
1032
+ // ── Diff ──────────────────────────────────────────────────────────────────
1033
+
1034
+ /**
1035
+ * Raw millisecond difference: `this - other`. Positive when this is later.
1036
+ * @category Difference
1037
+ */
1038
+ diffInMilliseconds(other: Carbon): number {
1039
+ return Number(this._zdt.epochMilliseconds - other._zdt.epochMilliseconds);
1040
+ }
1041
+
1042
+ /** Signed difference in seconds (fractional). @category Difference */
1043
+ diffInSeconds(other: Carbon): number {
1044
+ return this.diffInMilliseconds(other) / 1000;
1045
+ }
1046
+ /** Signed difference in minutes (fractional). @category Difference */
1047
+ diffInMinutes(other: Carbon): number {
1048
+ return this.diffInSeconds(other) / 60;
1049
+ }
1050
+ /** Signed difference in hours (fractional). @category Difference */
1051
+ diffInHours(other: Carbon): number {
1052
+ return this.diffInMinutes(other) / 60;
1053
+ }
1054
+ /** Signed difference in days (fractional). @category Difference */
1055
+ diffInDays(other: Carbon): number {
1056
+ return this.diffInHours(other) / 24;
1057
+ }
1058
+ /** Signed difference in weeks (fractional). @category Difference */
1059
+ diffInWeeks(other: Carbon): number {
1060
+ return this.diffInDays(other) / 7;
1061
+ }
1062
+
1063
+ /** Signed whole-month difference by calendar year/month fields. @category Difference */
1064
+ diffInMonths(other: Carbon): number {
1065
+ return (this.year - other.year) * 12 + (this.month - other.month);
1066
+ }
1067
+
1068
+ /**
1069
+ * Signed difference in whole years — the number a person would call an age.
1070
+ *
1071
+ * Derived from Temporal's calendar arithmetic rather than from subtracted year/month
1072
+ * fields, because the day matters: a Feb-29 birthday measured on 28 February 2024
1073
+ * subtracts to a whole number of months and reports the birthday as already passed. The
1074
+ * result truncates toward zero, so a birthday one day away is still the lower age.
1075
+ *
1076
+ * @param other - The date to measure from.
1077
+ * @returns Whole years from `other` to this instant; negative when `other` is later.
1078
+ * @category Difference
1079
+ */
1080
+ diffInYears(other: Carbon): number {
1081
+ const duration = other._zdt.until(this._zdt, { largestUnit: "year" });
1082
+ return duration.years;
1083
+ }
1084
+
1085
+ /**
1086
+ * Return the difference as a CarbonInterval (backed by Temporal.Duration).
1087
+ *
1088
+ * Uses Temporal's `until()` with the given largest unit so the result is
1089
+ * calendar-aware (months and years are counted properly).
1090
+ *
1091
+ * @example
1092
+ * ```ts
1093
+ * const interval = birthday.diffAsCarbonInterval(Carbon.now(), "year");
1094
+ * console.log(interval.years); // → 28
1095
+ * ```
1096
+ * @category Difference
1097
+ */
1098
+ diffAsCarbonInterval(other: Carbon, largestUnit: Temporal.DateTimeUnit = "day"): CarbonInterval {
1099
+ const duration = this._zdt.until(other._zdt, { largestUnit });
1100
+ return CarbonInterval.fromDuration(duration);
1101
+ }
1102
+
1103
+ // ── diffForHumans ─────────────────────────────────────────────────────────
1104
+
1105
+ /**
1106
+ * Human-readable relative phrasing via `Intl.RelativeTimeFormat`.
1107
+ *
1108
+ * With no argument, compares against now; pass another date to compare
1109
+ * against it. The largest matching unit is used by default; raise
1110
+ * `options.parts` for finer granularity.
1111
+ *
1112
+ * @param other - A date/`Carbon`/string to compare against, or the options object.
1113
+ * @param options - Formatting options (syntax, parts, absolute, join, locale, intl).
1114
+ * @returns A phrase such as `"3 days ago"`, `"in 2 hours"`, or `"just now"`.
1115
+ *
1116
+ * @example
1117
+ * ```ts
1118
+ * Carbon.now().subtractDays(3).diffForHumans(); // "3 days ago"
1119
+ * Carbon.now().addHours(2).diffForHumans(); // "in 2 hours"
1120
+ * a.diffForHumans(b, { parts: 2, absolute: true }); // "1 day, 4 hours"
1121
+ * ```
1122
+ * @category Difference
1123
+ */
1124
+ diffForHumans(): string;
1125
+ diffForHumans(options: DiffForHumansOptions): string;
1126
+ diffForHumans(other: Carbon | Date | string, options?: DiffForHumansOptions): string;
1127
+ diffForHumans(
1128
+ otherOrOptions?: Carbon | Date | string | DiffForHumansOptions,
1129
+ maybeOptions?: DiffForHumansOptions,
1130
+ ): string {
1131
+ let other: Carbon | null = null;
1132
+ let opts: DiffForHumansOptions = {};
1133
+
1134
+ if (
1135
+ otherOrOptions instanceof Carbon ||
1136
+ otherOrOptions instanceof Date ||
1137
+ typeof otherOrOptions === "string"
1138
+ ) {
1139
+ other = new Carbon(otherOrOptions as CarbonInput);
1140
+ opts = maybeOptions ?? {};
1141
+ } else if (otherOrOptions !== undefined) {
1142
+ opts = otherOrOptions;
1143
+ }
1144
+
1145
+ const {
1146
+ parts = 1,
1147
+ absolute = false,
1148
+ join = ", ",
1149
+ locale = "en",
1150
+ intl = { numeric: "auto" },
1151
+ } = opts;
1152
+
1153
+ const compareTo = other ?? Carbon.now(this._zdt.timeZoneId);
1154
+ const diffMs = Number(this._zdt.epochMilliseconds - compareTo._zdt.epochMilliseconds);
1155
+ const isNeg = diffMs < 0;
1156
+ const absDiff = Math.abs(diffMs);
1157
+
1158
+ const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
1159
+ ["year", 31_536_000_000],
1160
+ ["month", 2_592_000_000],
1161
+ ["week", 604_800_000],
1162
+ ["day", 86_400_000],
1163
+ ["hour", 3_600_000],
1164
+ ["minute", 60_000],
1165
+ ["second", 1_000],
1166
+ ];
1167
+
1168
+ const segments: { unit: Intl.RelativeTimeFormatUnit; value: number }[] = [];
1169
+ let remaining = absDiff;
1170
+
1171
+ for (const [unit, ms] of UNITS) {
1172
+ if (remaining >= ms) {
1173
+ const value = Math.floor(remaining / ms);
1174
+ remaining %= ms;
1175
+ segments.push({ unit, value });
1176
+ if (segments.length >= parts) break;
1177
+ }
1178
+ }
1179
+
1180
+ if (segments.length === 0) return "just now";
1181
+
1182
+ const fmt = new Intl.RelativeTimeFormat(locale, intl);
1183
+ const sign = opts.syntax ?? (isNeg ? "ago" : "from");
1184
+ const relSign = sign === "ago" ? -1 : 1;
1185
+
1186
+ const formatted = segments.map(({ unit, value }) =>
1187
+ absolute ? `${value} ${unit}${value !== 1 ? "s" : ""}` : fmt.format(relSign * value, unit),
1188
+ );
1189
+
1190
+ return formatted.join(join);
1191
+ }
1192
+
1193
+ // ── Formatting ────────────────────────────────────────────────────────────
1194
+
1195
+ /**
1196
+ * Token-based formatting:
1197
+ *
1198
+ * | Token | Meaning | Example |
1199
+ * |-------|----------------------------|----------|
1200
+ * | YYYY | 4-digit year | 2024 |
1201
+ * | YY | 2-digit year | 24 |
1202
+ * | MMMM | Full month name | January |
1203
+ * | MMM | Short month name | Jan |
1204
+ * | MM | 2-digit month | 01 |
1205
+ * | M | Month | 1 |
1206
+ * | DDDD | Full weekday | Monday |
1207
+ * | DDD | Short weekday | Mon |
1208
+ * | DD | 2-digit day | 05 |
1209
+ * | D | Day | 5 |
1210
+ * | HH | 24h hour (padded) | 09 |
1211
+ * | H | 24h hour | 9 |
1212
+ * | mm | Minutes (padded) | 04 |
1213
+ * | m | Minutes | 4 |
1214
+ * | ss | Seconds (padded) | 07 |
1215
+ * | s | Seconds | 7 |
1216
+ * | SSS | Milliseconds (3 digits) | 042 |
1217
+ * | Z | UTC offset (+05:30 / Z) | +05:30 |
1218
+ *
1219
+ * @param template - Token string; defaults to `"YYYY-MM-DD HH:mm:ss"`.
1220
+ * @example
1221
+ * ```ts
1222
+ * Carbon.now().format("MMM DD, YYYY"); // "Jun 09, 2026"
1223
+ * Carbon.now().format("HH:mm"); // "14:30"
1224
+ * ```
1225
+ * @category Formatting
1226
+ */
1227
+ format(template = "YYYY-MM-DD HH:mm:ss"): string {
1228
+ return this._format(template);
1229
+ }
1230
+
1231
+ /**
1232
+ * Format using native Intl.DateTimeFormat (locale-aware).
1233
+ *
1234
+ * @example
1235
+ * ```ts
1236
+ * Carbon.now().intlFormat("en-US", { dateStyle: "long" });
1237
+ * // → "June 9, 2026"
1238
+ * ```
1239
+ * @category Formatting
1240
+ */
1241
+ intlFormat(locale = "en-US", options: Intl.DateTimeFormatOptions = {}): string {
1242
+ // Defaults to this instance's zone, so the output agrees with every field getter.
1243
+ // Without it the formatter used the system zone and could name a different day
1244
+ // entirely. An explicit `timeZone` in `options` still wins.
1245
+ return new Intl.DateTimeFormat(locale, { timeZone: this.timezone, ...options }).format(
1246
+ this.toDate(),
1247
+ );
1248
+ }
1249
+
1250
+ // ── Serialisation ─────────────────────────────────────────────────────────
1251
+
1252
+ /** Return a native JS Date. @category Conversion */
1253
+ toDate(): Date {
1254
+ return new Date(Number(this._zdt.epochMilliseconds));
1255
+ }
1256
+
1257
+ /** ISO 8601 string with UTC offset, e.g. "2024-01-01T12:00:00+00:00". @category Conversion */
1258
+ toISOString(): string {
1259
+ return this._zdt.toInstant().toString();
1260
+ }
1261
+
1262
+ /** Return the backing Temporal.ZonedDateTime. @category Conversion */
1263
+ toZonedDateTime(): Temporal.ZonedDateTime {
1264
+ return this._zdt;
1265
+ }
1266
+
1267
+ /** Return a Temporal.Instant for this point in time. @category Conversion */
1268
+ toInstant(): Temporal.Instant {
1269
+ return this._zdt.toInstant();
1270
+ }
1271
+
1272
+ /** Return a Temporal.PlainDateTime (loses timezone info). @category Conversion */
1273
+ toPlainDateTime(): Temporal.PlainDateTime {
1274
+ return this._zdt.toPlainDateTime();
1275
+ }
1276
+
1277
+ /** Return a Temporal.PlainDate (loses time and timezone info). @category Conversion */
1278
+ toPlainDate(): Temporal.PlainDate {
1279
+ return this._zdt.toPlainDate();
1280
+ }
1281
+
1282
+ /** Unix timestamp in seconds. @category Conversion */
1283
+ toUnix(): number {
1284
+ return Math.floor(Number(this._zdt.epochMilliseconds) / 1000);
1285
+ }
1286
+
1287
+ /** Unix timestamp in milliseconds. @category Conversion */
1288
+ toMilliseconds(): number {
1289
+ return Number(this._zdt.epochMilliseconds);
1290
+ }
1291
+
1292
+ /** Epoch milliseconds — enables numeric coercion and `<`/`>` comparison. @category Conversion */
1293
+ valueOf(): number {
1294
+ return Number(this._zdt.epochMilliseconds);
1295
+ }
1296
+
1297
+ /** `"YYYY-MM-DD"` date string. @category Conversion */
1298
+ toDateString(): string {
1299
+ return this.format("YYYY-MM-DD");
1300
+ }
1301
+ /** `"YYYY-MM-DD HH:mm:ss"` date-time string. @category Conversion */
1302
+ toDateTimeString(): string {
1303
+ return this.format("YYYY-MM-DD HH:mm:ss");
1304
+ }
1305
+ /** `"HH:mm:ss"` time string. @category Conversion */
1306
+ toTimeString(): string {
1307
+ return this.format("HH:mm:ss");
1308
+ }
1309
+ /** Short date string, e.g. `"Jun 09, 2026"`. @category Conversion */
1310
+ toShortDate(): string {
1311
+ return this.format("MMM DD, YYYY");
1312
+ }
1313
+ /** Long date string, e.g. `"09 June 2026"`. @category Conversion */
1314
+ toLongDate(): string {
1315
+ return this.format("DD MMMM YYYY");
1316
+ }
1317
+
1318
+ /** ISO string returned when JSON.stringify is called. @category Conversion */
1319
+ toJSON(): string {
1320
+ return this._zdt.toInstant().toString();
1321
+ }
1322
+
1323
+ /** `"YYYY-MM-DD HH:mm:ss"` string used for string coercion. @category Conversion */
1324
+ toString(): string {
1325
+ return this.format("YYYY-MM-DD HH:mm:ss");
1326
+ }
1327
+
1328
+ /** ISO 8601 string — compatible with most database datetime columns. @category Conversion */
1329
+ toDatabase(): string {
1330
+ return this._zdt.toInstant().toString();
1331
+ }
1332
+
1333
+ // ── Private formatting impl ───────────────────────────────────────────────
1334
+
1335
+ private _format(template: string): string {
1336
+ const zdt = this._zdt;
1337
+ const pad = (value: number, length = 2) => value.toString().padStart(length, "0");
1338
+
1339
+ const MONTHS = [
1340
+ "January",
1341
+ "February",
1342
+ "March",
1343
+ "April",
1344
+ "May",
1345
+ "June",
1346
+ "July",
1347
+ "August",
1348
+ "September",
1349
+ "October",
1350
+ "November",
1351
+ "December",
1352
+ ];
1353
+ const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1354
+ // Temporal dayOfWeek: 1=Mon … 7=Sun → map to DAYS index
1355
+ const dayIndex = zdt.dayOfWeek % 7; // Mon(1)→1 … Sat(6)→6, Sun(7)→0
1356
+
1357
+ // UTC offset string e.g. "+05:30" or "Z"
1358
+ const offsetNs = zdt.offsetNanoseconds;
1359
+ const offsetMin = offsetNs / 60_000_000_000;
1360
+ const offsetStr =
1361
+ offsetNs === 0
1362
+ ? "Z"
1363
+ : `${offsetMin > 0 ? "+" : "-"}${pad(Math.floor(Math.abs(offsetMin) / 60))}:${pad(Math.abs(offsetMin) % 60)}`;
1364
+
1365
+ const tokens: Record<string, string> = {
1366
+ YYYY: zdt.year.toString(),
1367
+ YY: zdt.year.toString().slice(-2),
1368
+ MMMM: MONTHS[zdt.month - 1]!,
1369
+ MMM: MONTHS[zdt.month - 1]!.slice(0, 3),
1370
+ MM: pad(zdt.month),
1371
+ M: zdt.month.toString(),
1372
+ DDDD: DAYS[dayIndex]!,
1373
+ DDD: DAYS[dayIndex]!.slice(0, 3),
1374
+ DD: pad(zdt.day),
1375
+ D: zdt.day.toString(),
1376
+ HH: pad(zdt.hour),
1377
+ H: zdt.hour.toString(),
1378
+ mm: pad(zdt.minute),
1379
+ m: zdt.minute.toString(),
1380
+ ss: pad(zdt.second),
1381
+ s: zdt.second.toString(),
1382
+ SSS: pad(zdt.millisecond, 3),
1383
+ SS: pad(zdt.millisecond, 3).slice(0, 2),
1384
+ S: zdt.millisecond.toString().charAt(0) || "0",
1385
+ Z: offsetStr,
1386
+ };
1387
+
1388
+ const pattern = new RegExp(
1389
+ Object.keys(tokens)
1390
+ .sort((a, b) => b.length - a.length)
1391
+ .join("|"),
1392
+ "g",
1393
+ );
1394
+ return template.replace(pattern, (match) => tokens[match] ?? match);
1395
+ }
1396
+ }