@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.
- package/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- 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
|
+
}
|