@purple-brock/time 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +934 -2
- package/dist/Badger.d.ts +154 -0
- package/dist/BadgerSett.d.ts +45 -0
- package/dist/BadgerSettings.d.ts +14 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1568 -0
- package/dist/index.mjs +1564 -0
- package/dist/localeInfo.d.ts +10 -0
- package/package.json +60 -4
package/README.md
CHANGED
|
@@ -1,3 +1,935 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/lrprice79/badger-time/HEAD/assets/purple-brock-icon.png" width="140" alt="badger-time">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">badger-time</h1>
|
|
6
|
+
|
|
7
|
+
> The date library that actually understands where your users are sett.
|
|
8
|
+
|
|
9
|
+
A small, fast library for working with dates and times in JavaScript and TypeScript apps. No extra packages required — it works entirely with tools already built into browsers and Node.js. We dug deep so you don't have to.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What does it do?
|
|
14
|
+
|
|
15
|
+
Working with dates sounds simple — until your app needs to handle users in different countries, different time zones, and different date formats. Most date libraries assume everyone works Monday to Friday, weeks start on Monday, and times are shown in 24-hour format. That's wrong for a large part of the world.
|
|
16
|
+
|
|
17
|
+
**badger-time** handles this automatically:
|
|
18
|
+
|
|
19
|
+
- **Weekends differ by country.** In Saudi Arabia, the weekend is Friday and Saturday. In Israel it is also Friday and Saturday. In most of the world it is Saturday and Sunday. badger-time knows this for every locale (country/language setting) in the world.
|
|
20
|
+
- **Week starts differ by country.** In the US, weeks start on Sunday. In the UK and most of Europe, they start on Monday. badger-time picks the right one automatically.
|
|
21
|
+
- **Time format differs by country.** In the US, people use 12-hour clocks (3:30 PM). In Germany, 24-hour clocks (15:30). badger-time switches automatically based on locale.
|
|
22
|
+
- **Daylight saving time is handled correctly.** Calculating "start of today" or "days between two dates" can give wrong answers when clocks change. badger-time does this correctly.
|
|
23
|
+
|
|
24
|
+
You set your app's locale once at startup — every date operation and every formatted date in your app picks it up automatically.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @purple-brock/time
|
|
32
|
+
# or
|
|
33
|
+
yarn add @purple-brock/time
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Requires Node.js 20 or higher.** The library will throw an error on startup if you are running an older version.
|
|
37
|
+
|
|
38
|
+
> `formatDuration` and `formatCountdown` require Node 24+. Everything else in the library works on Node 20+.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## How does it compare to other libraries?
|
|
43
|
+
|
|
44
|
+
| Feature | moment | date-fns | luxon | badger-time |
|
|
45
|
+
|---|:---:|:---:|:---:|:---:|
|
|
46
|
+
| No extra packages needed | ✗ | ✗ | ✅ | ✅ |
|
|
47
|
+
| Dates are never modified in place | ✗ | ✅ | ✅ | ✅ |
|
|
48
|
+
| No `new` keyword required | ✗ | ✅ | ✗ | ✅ |
|
|
49
|
+
| Uses the platform's built-in locale tools — no separate language files | ✗ | ✗ | ✅ | ✅ |
|
|
50
|
+
| Correct day/week differences across clock changes | ✗ | ✗ | ✅ | ✅ |
|
|
51
|
+
| Correct start/end of day across clock changes | ✗ | via date-fns-tz | ✅ | ✅ |
|
|
52
|
+
| Create a date from a local time (not UTC) | ✗ | ✗ | ✅ | ✅ |
|
|
53
|
+
| Fractional differences (e.g. 1.5 hours) | ✅ | ✅ | ✅ | ✅ |
|
|
54
|
+
| Inclusive range options | ✅ | partial | ✗ | ✅ |
|
|
55
|
+
| Correct weekends for every country | ✗ | ✗ | ✗ | ✅ |
|
|
56
|
+
| Correct week start for every country | ✗ | ✗ | ✗ | ✅ |
|
|
57
|
+
| 12h/24h clock chosen automatically by country | ✗ | ✗ | ✗ | ✅ |
|
|
58
|
+
| Business day calculations per country | ✗ | ✗ | ✗ | ✅ |
|
|
59
|
+
| Date range generator | ✗ | ✗ | ✗ | ✅ |
|
|
60
|
+
| Countdown that returns `null` for past dates | ✗ | ✗ | ✗ | ✅ |
|
|
61
|
+
| Duration formatting in the user's language | ✗ | ✗ | ✗ | ✅ |
|
|
62
|
+
| Reactive global locale (one call updates the whole app) | ✗ | ✗ | ✗ | ✅ |
|
|
63
|
+
| Bundle size (minified + gzipped) | ~72 KB | ~14 KB | ~23 KB | **~5.1 KB** |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Badger, Sett, BadgerSettings } from "@purple-brock/time";
|
|
71
|
+
|
|
72
|
+
// Tell the library which country/language your app is running in.
|
|
73
|
+
// Do this once at startup — every date operation will use it automatically.
|
|
74
|
+
BadgerSettings.setLocale(new Intl.Locale("en-US"));
|
|
75
|
+
BadgerSettings.defaultTimeZone = "America/New_York";
|
|
76
|
+
|
|
77
|
+
// Add 7 days to a date
|
|
78
|
+
Badger("2026-06-15").add(7, "day").toISOString()
|
|
79
|
+
// "2026-06-22T00:00:00.000Z"
|
|
80
|
+
|
|
81
|
+
// Check if an order's delivery time is today
|
|
82
|
+
Badger(order.deliveryTime).isToday()
|
|
83
|
+
|
|
84
|
+
// Format a date for display — picks up locale automatically
|
|
85
|
+
Sett(order.deliveryTime).formatDate() // "06/15/2026, 03:30:45 PM"
|
|
86
|
+
Sett(order.deliveryTime).formatRelative() // "in 2 hours"
|
|
87
|
+
Sett(order.deliveryTime).formatTimeOnly() // "03:30:45 PM" (12-hour because en-US)
|
|
88
|
+
|
|
89
|
+
// Show a countdown — returns null automatically when the date has passed
|
|
90
|
+
Sett(new Date()).formatCountdown(new Date("2026-12-25"))
|
|
91
|
+
// "193 days, 4 hours, 22 minutes, and 11 seconds"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
### The three main exports
|
|
99
|
+
|
|
100
|
+
| Export | What it does |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `Badger(date?)` | Date arithmetic — add/subtract time, compare dates, check if a date is today/past/future, generate ranges, business day calculations |
|
|
103
|
+
| `Sett(date?, locale?, options?)` | Formatting — display dates in the user's language, relative time ("2 hours ago"), countdown timers, sending dates to a backend |
|
|
104
|
+
| `BadgerSettings` | Global defaults — set the locale and time zone once, subscribe to locale changes |
|
|
105
|
+
|
|
106
|
+
No `new` keyword is needed anywhere. Every operation returns a new value — the original date is never changed.
|
|
107
|
+
|
|
108
|
+
### TypeScript types
|
|
109
|
+
|
|
110
|
+
If you are using TypeScript, the factory names double as the instance types — so
|
|
111
|
+
`Badger` and `Sett` can be used as **both a value and a type**, with no separate
|
|
112
|
+
type import:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { Badger, Sett } from "@purple-brock/time";
|
|
116
|
+
|
|
117
|
+
function addBusinessDays(date: Badger, n: number): Badger {
|
|
118
|
+
return date.addBusinessDays(n);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatForDisplay(date: Badger): Sett {
|
|
122
|
+
return Sett(date.toDate());
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The explicit instance-type aliases `BadgerInstance` / `BadgerSettInstance` remain
|
|
127
|
+
exported and are equivalent — use whichever reads better:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import type { BadgerInstance, BadgerSettInstance } from "@purple-brock/time";
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Badger — working with dates
|
|
136
|
+
|
|
137
|
+
### Creating a date
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
Badger() // right now
|
|
141
|
+
Badger("2026-03-15T10:30:00") // a specific date and time (treated as UTC)
|
|
142
|
+
Badger("2026-03-15") // a specific date (local midnight)
|
|
143
|
+
Badger("2026-03-15T10:30:00Z") // explicit time zone offset — used as-is
|
|
144
|
+
Badger("2026-03-15T10:30:00+02:00") // with a +2 hour offset — used as-is
|
|
145
|
+
Badger(someDate) // copy of a JavaScript Date object
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**How strings are interpreted:**
|
|
149
|
+
- A date-only string like `"2026-03-15"` is anchored to **local midnight**, so its calendar day is correct in every time zone — reading `.day`, adding months, or building a range never drifts to the previous day for users west of UTC
|
|
150
|
+
- A string that already includes a time zone (ends in `Z` or `+02:00`) is used exactly as written
|
|
151
|
+
- Any other date-time string (no time zone) is treated as UTC
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### Creating a date from local time
|
|
156
|
+
|
|
157
|
+
Use `Badger.fromLocal()` when a date and time come from a form or picker and represent the *user's local time*, not UTC.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// The user selected "22 May 2026 at 00:15" in their local time zone
|
|
161
|
+
const dt = Badger.fromLocal("2026-05-22", "00:15");
|
|
162
|
+
dt.hour; // 0 — local midnight, not shifted
|
|
163
|
+
dt.dayOfWeek; // 5 — Friday in local time
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The date must be `"YYYY-MM-DD"` and the time must be `"HH:mm"`. An error is thrown if the format is wrong or if the date doesn't exist (e.g. `"2026-02-30"`).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Finding the earliest or latest date
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const earliest = Badger.min(deadline, today, cutoff);
|
|
174
|
+
const latest = Badger.max(deadline, today, cutoff);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Throws an error if called with no arguments or if any input is an invalid date. Filter out invalid dates first:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const valid = dates.filter(d => d.isValid());
|
|
181
|
+
const earliest = valid.length ? Badger.min(...valid) : null;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Generating a list of dates (range)
|
|
187
|
+
|
|
188
|
+
Returns an array of dates from `start` up to (but not including) `end`. Useful for building calendars, chart labels, and report headers.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// Every day in June 2026
|
|
192
|
+
Badger.range("2026-06-01", "2026-07-01", "day")
|
|
193
|
+
// → [Jun 1, Jun 2, … Jun 30] — 30 dates
|
|
194
|
+
|
|
195
|
+
// One entry per week across a quarter
|
|
196
|
+
Badger.range(startOfQuarter, endOfQuarter, "week")
|
|
197
|
+
|
|
198
|
+
// Monthly labels for a chart
|
|
199
|
+
Badger.range("2026-01-01", "2027-01-01", "month")
|
|
200
|
+
// → [Jan 1, Feb 1, Mar 1, … Dec 1]
|
|
201
|
+
|
|
202
|
+
// Strings, plain Dates, and Badger instances all work interchangeably
|
|
203
|
+
Badger.range(new Date("2026-06-01"), new Date("2026-06-08"), "day")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
> Monthly ranges re-anchor to the **start's day-of-month**, clamping per month rather than drifting. A range from January 31 yields Jan 31, Feb 28, Mar 31, Apr 30, May 31 — not a steady slide down to the 28th.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### Reading parts of a date
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const dt = Badger("2026-07-04T15:30:45.123Z");
|
|
214
|
+
|
|
215
|
+
dt.year // 2026
|
|
216
|
+
dt.month // 7 (1 = January, 12 = December)
|
|
217
|
+
dt.day // 4
|
|
218
|
+
dt.hour // 15
|
|
219
|
+
dt.minute // 30
|
|
220
|
+
dt.second // 45
|
|
221
|
+
dt.millisecond // 123
|
|
222
|
+
dt.dayOfWeek // 6 (0 = Sunday, 1 = Monday … 6 = Saturday, in local time)
|
|
223
|
+
dt.dayOfYear // 185 (which day of the year this is, 1–366)
|
|
224
|
+
dt.weekYear // 2026 (the ISO year for the week — may differ at year boundaries)
|
|
225
|
+
dt.quarter // 3 (1–4)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### Adding and subtracting time
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
dt.add(7, "day") // 7 days later
|
|
234
|
+
dt.subtract(1, "month") // 1 month earlier
|
|
235
|
+
dt.add(-2, "hour") // 2 hours earlier (negative numbers work)
|
|
236
|
+
|
|
237
|
+
// Apply several units at once
|
|
238
|
+
dt.add({ days: 7, hours: 2, minutes: 30 })
|
|
239
|
+
dt.subtract({ months: 1, days: 5 })
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Singular and plural both work: `"day"` and `"days"`, `"month"` and `"months"`, etc.
|
|
243
|
+
|
|
244
|
+
**Available units:** `"year"`, `"quarter"`, `"month"`, `"week"`, `"day"`, `"hour"`, `"minute"`, `"second"`, `"millisecond"`
|
|
245
|
+
|
|
246
|
+
> Adding months clamps to the last valid day of the target month — January 31 + 1 month becomes February 28 or 29, not March 3.
|
|
247
|
+
|
|
248
|
+
> Amounts must be **whole numbers**. A fractional amount (e.g. `add(1.5, "day")`) throws a `RangeError` rather than silently truncating to a whole unit — use a finer unit instead (`add(36, "hour")`).
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Changing one field
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
dt.set("year", 2030) // returns a new date with just the year changed
|
|
256
|
+
dt.set("month", 12) // 1-indexed — 12 is December
|
|
257
|
+
dt.set("hour", 9)
|
|
258
|
+
|
|
259
|
+
// Clamps to the last valid day if needed
|
|
260
|
+
Badger("2026-01-31").set("month", 2) // → February 28
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Throws an error for impossible values (e.g. month 0, hour 25), fractional values (e.g. `set("hour", 1.5)`), or `NaN`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Business days
|
|
268
|
+
|
|
269
|
+
This is where badger-time is unique. It uses built-in browser/Node tools to know which days are weekends for every country in the world — no hard-coded rules, no extra data files.
|
|
270
|
+
|
|
271
|
+
**Weekend rules are looked up in this order:** explicit locale argument → `BadgerSettings.defaultLocale` → Saturday + Sunday.
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
// Standard week (Saturday + Sunday weekend)
|
|
275
|
+
Badger("2026-06-12").isBusinessDay() // true (Friday)
|
|
276
|
+
Badger("2026-06-13").isBusinessDay() // false (Saturday)
|
|
277
|
+
Badger("2026-06-15").isWeekend() // false (Monday)
|
|
278
|
+
Badger("2026-06-20").isWeekend() // true (Saturday)
|
|
279
|
+
|
|
280
|
+
// Saudi Arabia uses a Friday + Saturday weekend
|
|
281
|
+
Badger("2026-06-12").isBusinessDay(new Intl.Locale("ar-SA")) // false (Friday is the weekend)
|
|
282
|
+
Badger("2026-06-14").isBusinessDay(new Intl.Locale("ar-SA")) // true (Sunday is a workday)
|
|
283
|
+
|
|
284
|
+
// Add business days — skips the correct weekends for the locale
|
|
285
|
+
// The count must be a whole number — 1.5 throws an error
|
|
286
|
+
Badger("2026-06-12").addBusinessDays(1) // Mon Jun 15 (skips Sat+Sun)
|
|
287
|
+
Badger("2026-06-12").addBusinessDays(1, new Intl.Locale("ar-SA")) // Sun Jun 14 (skips Fri+Sat)
|
|
288
|
+
Badger("2026-06-15").subtractBusinessDays(1) // Fri Jun 12
|
|
289
|
+
|
|
290
|
+
// Set the locale once — all business day methods pick it up
|
|
291
|
+
BadgerSettings.setLocale(new Intl.Locale("ar-SA"));
|
|
292
|
+
Badger("2026-06-12").isBusinessDay() // false — uses ar-SA automatically
|
|
293
|
+
Badger("2026-06-12").addBusinessDays(1) // Sun Jun 14
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
No other date library does this natively. Other libraries just don't give a badger.
|
|
297
|
+
|
|
298
|
+
#### Public holidays (optional)
|
|
299
|
+
|
|
300
|
+
Weekends are derived automatically; **public holidays are not** — no locale or
|
|
301
|
+
region string can tell you that Dec 25 is a UK bank holiday but not a Saudi one,
|
|
302
|
+
and badger-time deliberately ships **no holiday data** (that would be a country-
|
|
303
|
+
by-country calendar to maintain forever). Instead every business-day method takes
|
|
304
|
+
an optional set of holiday dates that *you* supply — typically resolved from a
|
|
305
|
+
source like [`date-holidays`](https://www.npmjs.com/package/date-holidays), keyed
|
|
306
|
+
off the user's region. Holidays are then skipped exactly like weekends:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
const holidays = ["2026-12-25", "2026-12-28"]; // your dates, e.g. from date-holidays
|
|
310
|
+
|
|
311
|
+
Badger("2026-12-25").isBusinessDay(new Intl.Locale("en-GB"), holidays); // false
|
|
312
|
+
Badger("2026-12-24").addBusinessDays(1, new Intl.Locale("en-GB"), holidays); // Dec 29
|
|
313
|
+
Badger("2026-12-29").subtractBusinessDays(1, new Intl.Locale("en-GB"), holidays); // Dec 24
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Rules that keep this safe:
|
|
317
|
+
|
|
318
|
+
- **Strict ISO only.** Each entry must be a `YYYY-MM-DD` string. A typo, a
|
|
319
|
+
datetime, or a localized format (`25/12/2026`, `12/25/2026`) throws a
|
|
320
|
+
`RangeError` at the boundary rather than silently never matching — and so do
|
|
321
|
+
impossible dates (`2026-13-25`, `2026-02-30`). Convert any localized user input
|
|
322
|
+
to ISO *before* passing it in; the library never guesses `DD/MM` vs `MM/DD`.
|
|
323
|
+
- **Accepts an array or a `Set`** (anything `Iterable<string>`). Omitted or empty
|
|
324
|
+
means "weekends only" — fully backward compatible.
|
|
325
|
+
- **Local-date keyed.** A holiday matches the date's *local* calendar day,
|
|
326
|
+
consistent with the locale weekend evaluation (so it's correct near midnight in
|
|
327
|
+
any timezone, not just UTC).
|
|
328
|
+
- **Never hangs.** A pathological set that blocks every reachable day makes
|
|
329
|
+
`addBusinessDays`/`subtractBusinessDays` throw a `RangeError` instead of looping
|
|
330
|
+
forever.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### Countdown
|
|
335
|
+
|
|
336
|
+
Returns the time remaining until a future date as days, hours, minutes, and seconds. Returns `null` if the date has already passed — so you always know you need to handle the "expired" case.
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
Badger("2026-12-25").countdown()
|
|
340
|
+
// { days: 193, hours: 4, minutes: 22, seconds: 11 }
|
|
341
|
+
|
|
342
|
+
Badger("2025-01-01").countdown()
|
|
343
|
+
// null — already passed
|
|
344
|
+
|
|
345
|
+
// Useful in tests: provide a fixed "now" instead of the real current time
|
|
346
|
+
Badger("2026-12-25").countdown(new Date("2026-06-01T00:00:00Z"))
|
|
347
|
+
// { days: 207, hours: 0, minutes: 0, seconds: 0 }
|
|
348
|
+
|
|
349
|
+
// Pair with Sett.formatCountdown() to get a display string
|
|
350
|
+
Sett(new Date()).formatCountdown(new Date("2026-12-25"))
|
|
351
|
+
// "193 days, 4 hours, 22 minutes, and 11 seconds"
|
|
352
|
+
// null if the date has passed
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
### Start and end of a period
|
|
358
|
+
|
|
359
|
+
All operations use **local time** and never change the original date.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
dt.startOf("year") // 1 Jan, midnight (local time)
|
|
363
|
+
dt.startOf("quarter") // first day of the quarter, midnight
|
|
364
|
+
dt.startOf("month") // first of the month, midnight
|
|
365
|
+
dt.startOf("week") // Monday 00:00:00 (ISO 8601 standard)
|
|
366
|
+
dt.startOf("day") // midnight (00:00:00.000)
|
|
367
|
+
dt.startOf("hour") // top of the hour
|
|
368
|
+
dt.startOf("minute")
|
|
369
|
+
dt.startOf("second")
|
|
370
|
+
|
|
371
|
+
dt.endOf("year") // 31 Dec, 23:59:59.999 (local time)
|
|
372
|
+
dt.endOf("quarter")
|
|
373
|
+
dt.endOf("month") // last day of the month, 23:59:59.999
|
|
374
|
+
dt.endOf("week") // Sunday 23:59:59.999
|
|
375
|
+
dt.endOf("day") // 23:59:59.999
|
|
376
|
+
dt.endOf("hour")
|
|
377
|
+
dt.endOf("minute")
|
|
378
|
+
dt.endOf("second")
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
### Comparing dates
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
dt.isBefore(other)
|
|
387
|
+
dt.isAfter(other)
|
|
388
|
+
dt.isSameOrBefore(other)
|
|
389
|
+
dt.isSameOrAfter(other)
|
|
390
|
+
dt.isSame(other) // same exact millisecond
|
|
391
|
+
dt.isSame(other, "day") // same calendar day
|
|
392
|
+
dt.isSame(other, "month") // same month
|
|
393
|
+
dt.isSame(other, "week") // same ISO week (handles year boundaries correctly)
|
|
394
|
+
dt.isSame(other, "quarter")
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Checking if a date is within a range:**
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
// By default, the boundaries themselves are not included
|
|
401
|
+
date.isBetween(open, close)
|
|
402
|
+
|
|
403
|
+
// Include the start boundary
|
|
404
|
+
open.isBetween(open, close, { includeStart: true })
|
|
405
|
+
|
|
406
|
+
// Include the end boundary
|
|
407
|
+
close.isBetween(open, close, { includeEnd: true })
|
|
408
|
+
|
|
409
|
+
// Include both
|
|
410
|
+
date.isBetween(open, close, { includeStart: true, includeEnd: true })
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Throws an error if either boundary is invalid, or if `start` is after `end`.
|
|
414
|
+
|
|
415
|
+
**Clamping a date to a range:**
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
// Returns the closest valid date within the range
|
|
419
|
+
Badger("2025-01-01").clamp(min, max) // → min (too early, clamped to start)
|
|
420
|
+
Badger("2027-01-01").clamp(min, max) // → max (too late, clamped to end)
|
|
421
|
+
Badger("2026-06-15").clamp(min, max) // → unchanged (already within range)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Quick helpers:**
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
dt.isToday() // is this date today (in local time)?
|
|
428
|
+
dt.isYesterday()
|
|
429
|
+
dt.isTomorrow()
|
|
430
|
+
dt.isPast() // is this date before right now?
|
|
431
|
+
dt.isFuture() // is this date after right now?
|
|
432
|
+
dt.isWeekend() // is this a weekend day? (respects BadgerSettings.defaultLocale)
|
|
433
|
+
dt.isWeekday() // is this a working day? (respects BadgerSettings.defaultLocale)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
All comparisons accept either a `Badger` instance or a plain JavaScript `Date`.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
### Difference between two dates
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
a.diff(b, "hour") // 1 — whole hours, rounded down
|
|
444
|
+
a.diff(b, "hour", true) // 1.5 — fractional (pass true as the third argument)
|
|
445
|
+
b.diff(a, "minute") // -90 — negative when b is before a
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Day and week differences account for daylight saving time correctly — they count calendar days, not raw hours.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
### Other utilities
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
dt.toISOString() // "2026-03-15T10:30:00.000Z" — UTC instant; throws if invalid
|
|
456
|
+
dt.toLocalISODate() // "2026-03-15" — LOCAL calendar date; use this, never toISOString().slice(0,10)
|
|
457
|
+
dt.toDate() // a plain JavaScript Date (a copy — changes won't affect the Badger)
|
|
458
|
+
dt.toUnixTimestamp() // seconds since 1 Jan 1970 (floored)
|
|
459
|
+
dt.toMillisecondTimestamp() // milliseconds since 1 Jan 1970
|
|
460
|
+
dt.toObject() // { year, month, day, hour, minute, second, millisecond }
|
|
461
|
+
dt.clone() // independent copy
|
|
462
|
+
dt.isValid() // false if the date is corrupt or unparseable — always check this
|
|
463
|
+
dt.getDaysInMonth() // how many days are in the date's month (28–31)
|
|
464
|
+
dt.daysInYear() // 365 or 366 (NaN if invalid)
|
|
465
|
+
dt.getWeekNumber() // ISO 8601 week number (1–53)
|
|
466
|
+
dt.isLeapYear() // is this a leap year?
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### Invalid dates
|
|
472
|
+
|
|
473
|
+
Always call `isValid()` before using a `Badger` that was created from user input or an external source. When a date is invalid, methods return safe fallback values rather than crashing:
|
|
474
|
+
|
|
475
|
+
| Type of method | Examples | Returns when invalid |
|
|
476
|
+
|---|---|---|
|
|
477
|
+
| Yes/no checks | `isWeekend`, `isBusinessDay`, `isLeapYear`, `isPast`, `isBefore`… | `false` |
|
|
478
|
+
| Number values | `year`, `month`, `day`, `diff`, `toUnixTimestamp`… | `NaN` |
|
|
479
|
+
| Countdown | `countdown()` | `null` |
|
|
480
|
+
| Arithmetic | `add`, `subtract`, `startOf`, `endOf`, `clone` | An invalid Badger (further chaining stays invalid) |
|
|
481
|
+
| Business day iteration | `addBusinessDays`, `subtractBusinessDays` | Invalid clone — no looping |
|
|
482
|
+
| `toISOString()` / `toLocalISODate()` | — | Throws an error (there is no meaningful string to produce) |
|
|
483
|
+
| `Sett` formatters | `formatDate`, `formatTimeOnly`, `getDayName`… | Throw an error |
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
const bad = Badger("not-a-date");
|
|
487
|
+
bad.isValid() // false — check this first
|
|
488
|
+
bad.isWeekend() // false
|
|
489
|
+
bad.countdown() // null
|
|
490
|
+
bad.daysInYear() // NaN
|
|
491
|
+
bad.addBusinessDays(5) // returns an invalid clone — no looping happens
|
|
492
|
+
|
|
493
|
+
// toISOString is the one that throws — it can't produce a string for an invalid date
|
|
494
|
+
bad.toISOString() // Error: Badger.toISOString(): date is not valid…
|
|
495
|
+
|
|
496
|
+
// Badger.min and Badger.max also throw on invalid input — filter first
|
|
497
|
+
const dates = [Badger("2026-01-01"), Badger("bad"), Badger("2026-06-15")];
|
|
498
|
+
const valid = dates.filter(d => d.isValid());
|
|
499
|
+
Badger.min(...valid) // Badger("2026-01-01")
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Sett — displaying and formatting dates
|
|
505
|
+
|
|
506
|
+
`Sett` turns a date into a string your users can read. It uses the platform's built-in formatting tools, so it automatically uses the right format for the user's country — no extra language files, no manual configuration per locale.
|
|
507
|
+
|
|
508
|
+
Defaults to British English (`en-GB`) and London time unless you override it with `BadgerSettings`.
|
|
509
|
+
|
|
510
|
+
### Creating a Sett
|
|
511
|
+
|
|
512
|
+
String inputs follow the same rules as `Badger()` — bare date-time strings are treated as UTC.
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
Sett() // now, global defaults
|
|
516
|
+
Sett(someDate) // specific date, global defaults
|
|
517
|
+
Sett("2026-03-15T10:30:00") // string input → treated as UTC
|
|
518
|
+
Sett("2026-03-15") // date-only → LOCAL midnight
|
|
519
|
+
Sett(someDate, new Intl.Locale("sv-SE")) // override the locale
|
|
520
|
+
Sett(someDate, new Intl.Locale("sv-SE"), { // override locale + formatting options
|
|
521
|
+
timeZone: "Europe/Stockholm",
|
|
522
|
+
year: "numeric",
|
|
523
|
+
month: "2-digit",
|
|
524
|
+
day: "2-digit"
|
|
525
|
+
})
|
|
526
|
+
Sett(someDate, "sv-SE") // a plain locale string works too
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Hermes caveat:** `formatRelative` needs `Intl.RelativeTimeFormat`, and `formatCountdown` /
|
|
530
|
+
`formatDuration` need `Intl.DurationFormat`. Hermes (React Native) doesn't document either, so treat
|
|
531
|
+
those three as **web/Node only**. Everything else is Hermes-safe. `formatRelative` rounds to the
|
|
532
|
+
nearest month (31 Jan → 1 Mar is "last month").
|
|
533
|
+
|
|
534
|
+
**Time zone default:** a `Sett` with no `timeZone` option uses `BadgerSettings.defaultTimeZone`, else
|
|
535
|
+
the **device's own zone** (it used to fall back to Europe/London).
|
|
536
|
+
|
|
537
|
+
**React Native / Hermes:** Hermes has no `Intl.Locale`. badger-time never needs one: the
|
|
538
|
+
default locale is the plain tag `"en-GB"`, and anything that wants week info (`isWeekend`,
|
|
539
|
+
`startOfWeek`, `getDaysOfTheWeek`, the business-day methods) or the 12/24-hour clock
|
|
540
|
+
(`formatTimeOnly`) reads it safely, falling back to **Monday-first, Saturday + Sunday weekend**
|
|
541
|
+
when `Intl.Locale` is missing. On Hermes, pass locales as **strings** (`"sv-SE"`) rather than
|
|
542
|
+
`new Intl.Locale(...)`. Every locale parameter accepts either form (`LocaleInput`).
|
|
543
|
+
|
|
544
|
+
### Displaying a date
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
const s = Sett(new Date("2026-03-15T14:30:45Z"));
|
|
548
|
+
|
|
549
|
+
s.formatDate() // "15/03/2026, 14:30:45" (en-GB format, 24-hour)
|
|
550
|
+
s.formatDateOnly() // "15/03/2026"
|
|
551
|
+
s.formatTimeOnly() // "14:30:45" (24-hour for en-GB, chosen automatically)
|
|
552
|
+
s.getDayName() // "Sunday" (pass "short" → "Sun", "narrow" → "S")
|
|
553
|
+
s.getMonthName() // "March" (pass "short" → "Mar", "narrow" → "M")
|
|
554
|
+
s.getDaysOfTheWeek() // ["Monday", "Tuesday", …] — starts on the locale's first day
|
|
555
|
+
s.getMonthsOfTheYear() // ["January", …, "December"]
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 12-hour vs 24-hour clocks — automatic
|
|
559
|
+
|
|
560
|
+
`formatTimeOnly()` reads the locale and picks the right clock format. You don't need to configure anything.
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
// US English — 12-hour clock
|
|
564
|
+
Sett(new Date("2026-06-15T15:30:45Z"), new Intl.Locale("en-US"), { timeZone: "UTC" }).formatTimeOnly()
|
|
565
|
+
// "03:30:45 PM"
|
|
566
|
+
|
|
567
|
+
// German — 24-hour clock
|
|
568
|
+
Sett(new Date("2026-06-15T15:30:45Z"), new Intl.Locale("de-DE"), { timeZone: "UTC" }).formatTimeOnly()
|
|
569
|
+
// "15:30:45"
|
|
570
|
+
|
|
571
|
+
// Arabic (Saudi Arabia) — 12-hour in Arabic numerals
|
|
572
|
+
Sett(new Date("2026-06-15T15:30:45Z"), new Intl.Locale("ar-SA"), { timeZone: "UTC" }).formatTimeOnly()
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Locale-aware calendar helpers
|
|
576
|
+
|
|
577
|
+
`isWeekend()` and `startOfWeek()` use the platform's built-in country data — correct for every country worldwide. Both use local time, so they give the right answer even near midnight in non-UTC time zones.
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
// Saudi Arabia: Friday and Saturday are the weekend
|
|
581
|
+
Sett(new Date("2026-06-05"), new Intl.Locale("ar-SA")).isWeekend() // true (Friday)
|
|
582
|
+
Sett(new Date("2026-06-07"), new Intl.Locale("ar-SA")).isWeekend() // false (Sunday is a workday)
|
|
583
|
+
|
|
584
|
+
// Where does the week start?
|
|
585
|
+
Sett(new Date("2026-06-10T12:00:00"), new Intl.Locale("en-US")).startOfWeek()
|
|
586
|
+
// → Midnight on Sunday Jun 7 (US weeks start Sunday)
|
|
587
|
+
|
|
588
|
+
Sett(new Date("2026-06-10T12:00:00"), new Intl.Locale("en-GB")).startOfWeek()
|
|
589
|
+
// → Midnight on Monday Jun 8 (UK weeks start Monday)
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Relative time
|
|
593
|
+
|
|
594
|
+
Shows how long ago or how far in the future a date is — like "3 hours ago" or "in 2 days".
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
s.formatRelative() // "3 hours ago" / "in 2 days" / "last month"
|
|
598
|
+
s.formatRelative(anchorDate) // relative to a specific date instead of now
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Month and year thresholds are based on elapsed days, not calendar month boundaries. A 7-day gap that crosses a month boundary shows as "last week", not "last month". 28+ days in different months shows as months.
|
|
602
|
+
|
|
603
|
+
Throws an error if the date is invalid — never silently shows garbage.
|
|
604
|
+
|
|
605
|
+
### Duration formatting
|
|
606
|
+
|
|
607
|
+
Shows a duration in the user's natural language. Uses the platform's built-in tools — no hand-rolled formatting.
|
|
608
|
+
|
|
609
|
+
> Requires Node 24+, Chrome 127+, Firefox 127+, or Safari 17.4+. If you call this on an older environment it throws — but the rest of `Sett` continues to work fine on Node 20+.
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
Sett(new Date(), new Intl.Locale("en-GB")).formatDuration({ hours: 3, minutes: 22 })
|
|
613
|
+
// "3 hours and 22 minutes"
|
|
614
|
+
|
|
615
|
+
Sett(new Date(), new Intl.Locale("de-DE")).formatDuration({ hours: 2, minutes: 30 })
|
|
616
|
+
// "2 Stunden und 30 Minuten" (German)
|
|
617
|
+
|
|
618
|
+
Sett(new Date(), new Intl.Locale("fr-FR")).formatDuration({ days: 1, hours: 4 })
|
|
619
|
+
// "1 jour et 4 heures" (French)
|
|
620
|
+
|
|
621
|
+
// Calculate elapsed time and format in one go
|
|
622
|
+
const minutes = Badger(startTime).diff(Badger(), "minute");
|
|
623
|
+
Sett(new Date()).formatDuration({ minutes })
|
|
624
|
+
// "47 minutes"
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Countdown display
|
|
628
|
+
|
|
629
|
+
Combines the countdown calculation with duration formatting in a single call. Returns `null` if the date has passed.
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
Sett(new Date()).formatCountdown(new Date("2026-12-25"))
|
|
633
|
+
// "193 days, 4 hours, 22 minutes, and 11 seconds"
|
|
634
|
+
|
|
635
|
+
Sett(new Date()).formatCountdown(new Date("2025-01-01"))
|
|
636
|
+
// null — date is in the past
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
Live countdown timer in React Native — updates every second, switches to `"Expired"` when done:
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
function CountdownLabel({ endsAt }: { endsAt: string }) {
|
|
643
|
+
const [label, setLabel] = useState<string | null>(null);
|
|
644
|
+
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
const tick = () => setLabel(
|
|
647
|
+
Sett(new Date()).formatCountdown(new Date(endsAt)) ?? "Expired"
|
|
648
|
+
);
|
|
649
|
+
tick();
|
|
650
|
+
const id = setInterval(tick, 1000);
|
|
651
|
+
return () => clearInterval(id);
|
|
652
|
+
}, [endsAt]);
|
|
653
|
+
|
|
654
|
+
return <Text>{label}</Text>;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Sending dates to a backend server
|
|
659
|
+
|
|
660
|
+
These methods convert a date to a standard format (UTC ISO 8601) suitable for sending to a server. Start/end of day calculations account for daylight saving time — the correct UTC offset is resolved for that specific date, not approximated.
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
s.formatForBackend() // "2026-03-15T10:30:45.000Z"
|
|
664
|
+
s.formatForBackendToStartOfDay() // midnight in the configured time zone → UTC
|
|
665
|
+
s.formatForBackendToEndOfDay() // 23:59:59.999 in the configured time zone → UTC
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
// London is UTC+1 in summer (BST) — midnight on 15 Jul = 23:00 UTC on 14 Jul
|
|
670
|
+
Sett("2026-07-15T12:00:00Z").formatForBackendToStartOfDay()
|
|
671
|
+
// "2026-07-14T23:00:00.000Z"
|
|
672
|
+
|
|
673
|
+
// London is UTC+0 in winter (GMT) — midnight on 15 Jan = 00:00 UTC
|
|
674
|
+
Sett("2026-01-15T12:00:00Z").formatForBackendToStartOfDay()
|
|
675
|
+
// "2026-01-15T00:00:00.000Z"
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## BadgerSettings — global defaults
|
|
681
|
+
|
|
682
|
+
Set your app's locale and time zone once at startup. Every `Sett()` and `Badger()` call will inherit these automatically, unless you override them at the call site.
|
|
683
|
+
|
|
684
|
+
```ts
|
|
685
|
+
import { BadgerSettings } from "@purple-brock/time";
|
|
686
|
+
|
|
687
|
+
BadgerSettings.defaultLocale = new Intl.Locale("sv-SE");
|
|
688
|
+
BadgerSettings.defaultTimeZone = "Europe/Stockholm";
|
|
689
|
+
|
|
690
|
+
// No locale argument needed anywhere in the app
|
|
691
|
+
Sett(order.deliveryTime).formatDate() // formatted in sv-SE / Stockholm
|
|
692
|
+
Sett(order.deliveryTime).isWeekend() // uses Swedish weekend rules
|
|
693
|
+
Sett(order.deliveryTime).formatTimeOnly() // 24-hour (Sweden uses 24-hour clocks)
|
|
694
|
+
|
|
695
|
+
// Badger picks up the locale too
|
|
696
|
+
Badger("2026-06-12").isBusinessDay() // uses Swedish weekend definition
|
|
697
|
+
Badger("2026-06-12").addBusinessDays(3) // skips Swedish weekends
|
|
698
|
+
|
|
699
|
+
// You can always override the global locale for a single call
|
|
700
|
+
Sett(date, new Intl.Locale("fr-FR")).formatDate() // → French format, ignores global
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Reactive language switching
|
|
704
|
+
|
|
705
|
+
`setLocale()` updates the global locale and immediately notifies all active listeners. One call, entire app updates.
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
// Listen for locale changes
|
|
709
|
+
const unsub = BadgerSettings.subscribe(locale => {
|
|
710
|
+
console.log("Switched to", locale.baseName);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
BadgerSettings.setLocale(new Intl.Locale("fr-FR"));
|
|
714
|
+
// → "Switched to fr-FR"
|
|
715
|
+
|
|
716
|
+
unsub(); // stop listening — important to clean up in components
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### React Native
|
|
720
|
+
|
|
721
|
+
```tsx
|
|
722
|
+
// localeContext.tsx — wrap your app with this so any component can get the current locale
|
|
723
|
+
import { createContext, useContext, useState, useEffect } from "react";
|
|
724
|
+
import { BadgerSettings } from "@purple-brock/time";
|
|
725
|
+
|
|
726
|
+
const LocaleContext = createContext<Intl.Locale>(new Intl.Locale("en-GB"));
|
|
727
|
+
|
|
728
|
+
export function LocaleProvider({ children }) {
|
|
729
|
+
const [locale, setLocale] = useState(
|
|
730
|
+
BadgerSettings.defaultLocale ?? new Intl.Locale("en-GB")
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
// Subscribe to locale changes — unsubscribes automatically on unmount
|
|
734
|
+
useEffect(() => BadgerSettings.subscribe(setLocale), []);
|
|
735
|
+
|
|
736
|
+
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export const useLocale = () => useContext(LocaleContext);
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
```tsx
|
|
743
|
+
// A language switcher button — one press updates every date in the app
|
|
744
|
+
<Button
|
|
745
|
+
title="Français"
|
|
746
|
+
onPress={() => BadgerSettings.setLocale(new Intl.Locale("fr-FR"))}
|
|
747
|
+
/>
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
// Any component — automatically re-renders when the locale changes
|
|
752
|
+
function OrderCard({ order }) {
|
|
753
|
+
const locale = useLocale();
|
|
754
|
+
return (
|
|
755
|
+
<View>
|
|
756
|
+
<Text>{Sett(order.deliveryTime, locale).formatDate()}</Text>
|
|
757
|
+
<Text>{Sett(order.deliveryTime, locale).formatRelative()}</Text>
|
|
758
|
+
</View>
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### React (web)
|
|
764
|
+
|
|
765
|
+
```tsx
|
|
766
|
+
// Restore the saved locale when the app loads
|
|
767
|
+
useEffect(() => {
|
|
768
|
+
const saved = localStorage.getItem("locale");
|
|
769
|
+
if (saved) BadgerSettings.setLocale(new Intl.Locale(saved));
|
|
770
|
+
}, []);
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Vue
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
export const appLocale = ref(new Intl.Locale("en-GB"));
|
|
777
|
+
BadgerSettings.subscribe(locale => { appLocale.value = locale; });
|
|
778
|
+
|
|
779
|
+
// In a component
|
|
780
|
+
const formatted = computed(() => Sett(order.deliveryTime, appLocale.value).formatDate());
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### Vanilla JS
|
|
784
|
+
|
|
785
|
+
```ts
|
|
786
|
+
BadgerSettings.subscribe(locale => {
|
|
787
|
+
document.querySelectorAll<HTMLElement>("[data-timestamp]").forEach(el => {
|
|
788
|
+
el.textContent = Sett(new Date(el.dataset.timestamp!), locale).formatRelative();
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### Resetting (for tests)
|
|
794
|
+
|
|
795
|
+
Clears the locale, time zone, and all subscribers. Essential in tests to prevent settings from leaking between test cases.
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
afterEach(() => BadgerSettings.reset());
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## Real-world examples
|
|
804
|
+
|
|
805
|
+
### Delivery app — order card
|
|
806
|
+
|
|
807
|
+
```ts
|
|
808
|
+
const order = { dueAt: "2026-06-15T18:30:00Z", locale: new Intl.Locale("en-GB") };
|
|
809
|
+
|
|
810
|
+
// Is the order due today?
|
|
811
|
+
Badger(order.dueAt).isToday()
|
|
812
|
+
|
|
813
|
+
// How many minutes until it's due?
|
|
814
|
+
Badger(order.dueAt).diff(Badger(), "minute")
|
|
815
|
+
|
|
816
|
+
// Display the due time in the customer's locale (12h/24h chosen automatically)
|
|
817
|
+
Sett(order.dueAt, order.locale).formatTimeOnly()
|
|
818
|
+
|
|
819
|
+
// Show a countdown — or null if already past due
|
|
820
|
+
Sett(new Date(), order.locale).formatCountdown(new Date(order.dueAt))
|
|
821
|
+
// "3 hours and 22 minutes"
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### HR system — notice periods
|
|
825
|
+
|
|
826
|
+
```ts
|
|
827
|
+
// Add 30 business days, skipping the correct weekends for the employee's country
|
|
828
|
+
const lastDay = Badger(terminationDate).addBusinessDays(30, employeeLocale);
|
|
829
|
+
|
|
830
|
+
// Count remaining working days
|
|
831
|
+
const workingDays = Badger.range(Badger(), lastDay, "day")
|
|
832
|
+
.filter(d => d.isBusinessDay(employeeLocale)).length;
|
|
833
|
+
|
|
834
|
+
// Display the last working day
|
|
835
|
+
Sett(lastDay.toDate(), employeeLocale).formatDateOnly()
|
|
836
|
+
// "Monday 27 July 2026"
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
### Calendar UI — month grid
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
// Get every day in June 2026
|
|
843
|
+
const days = Badger.range(
|
|
844
|
+
Badger("2026-06-01").startOf("month"),
|
|
845
|
+
Badger("2026-06-01").endOf("month").add(1, "day"),
|
|
846
|
+
"day"
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
// Week start and day names vary by country
|
|
850
|
+
const weekStart = Sett(new Date(), userLocale).startOfWeek();
|
|
851
|
+
const weekDayNames = Sett(new Date(), userLocale).getDaysOfTheWeek("short");
|
|
852
|
+
// en-GB: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
853
|
+
// en-US: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### Event countdown — live timer
|
|
857
|
+
|
|
858
|
+
```tsx
|
|
859
|
+
function EventBanner({ eventDate }: { eventDate: string }) {
|
|
860
|
+
const [label, setLabel] = useState<string | null>(null);
|
|
861
|
+
|
|
862
|
+
useEffect(() => {
|
|
863
|
+
const tick = () => setLabel(
|
|
864
|
+
Sett(new Date()).formatCountdown(new Date(eventDate)) ?? "Event has started!"
|
|
865
|
+
);
|
|
866
|
+
tick();
|
|
867
|
+
const id = setInterval(tick, 1000);
|
|
868
|
+
return () => clearInterval(id);
|
|
869
|
+
}, [eventDate]);
|
|
870
|
+
|
|
871
|
+
return <Text>{label}</Text>;
|
|
872
|
+
}
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### Backend query — this week's data
|
|
876
|
+
|
|
877
|
+
```ts
|
|
878
|
+
const locale = new Intl.Locale("en-GB");
|
|
879
|
+
const tz = "Europe/London";
|
|
880
|
+
const opts = {
|
|
881
|
+
timeZone: tz,
|
|
882
|
+
year: "numeric" as const, month: "2-digit" as const,
|
|
883
|
+
day: "2-digit" as const, hour: "2-digit" as const,
|
|
884
|
+
minute: "2-digit" as const, second: "2-digit" as const,
|
|
885
|
+
hour12: false
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// Get the correct start and end of this week, accounting for clock changes
|
|
889
|
+
const weekStart = Sett(new Date(), locale, opts);
|
|
890
|
+
const weekEnd = Sett(Badger().endOf("week").toDate(), locale, opts);
|
|
891
|
+
|
|
892
|
+
const from = weekStart.formatForBackendToStartOfDay();
|
|
893
|
+
const to = weekEnd.formatForBackendToEndOfDay();
|
|
894
|
+
|
|
895
|
+
await fetch(`/api/orders?from=${from}&to=${to}`);
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### Comparing dates across time zones
|
|
899
|
+
|
|
900
|
+
```ts
|
|
901
|
+
// Server returns UTC timestamps — badger-time handles them correctly
|
|
902
|
+
const serverDate = Badger("2026-06-15T22:00:00Z");
|
|
903
|
+
|
|
904
|
+
// A user in Central European Summer Time (UTC+2) sees this as midnight on Jun 16
|
|
905
|
+
serverDate.isToday() // correct for the user's local time zone
|
|
906
|
+
serverDate.dayOfWeek // correct local day of week
|
|
907
|
+
serverDate.isWeekend() // correct local weekend check
|
|
908
|
+
|
|
909
|
+
// The backend always receives UTC, regardless of the user's time zone
|
|
910
|
+
Sett(serverDate.toDate()).formatForBackend()
|
|
911
|
+
// "2026-06-15T22:00:00.000Z"
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
916
|
+
## Quality
|
|
917
|
+
|
|
918
|
+
- **817 tests** (8 skipped on Node < 24, where `formatDuration`/`formatCountdown` are unavailable) — every public method, every unit, edge cases including clock change transitions, year boundaries, ISO week rollovers, and locale-specific weekend/week-start logic, plus type-level tests that lock the public type surface
|
|
919
|
+
- **100% coverage** — statements, branches, functions, and lines
|
|
920
|
+
- **Four time zone suites** — tests run under Europe/Berlin, America/New_York, America/Los_Angeles, and UTC to verify calculations in UTC+ and UTC− zones. New York and Los Angeles cover spring-forward (March) and fall-back (November). Los Angeles also guards the date-only-string path west of UTC, where a mis-anchored parse would drift the calendar day. Kolkata (UTC+5:30, no clock changes) covers the half-hour offset across all months and month-ends
|
|
921
|
+
- **Structured audit** — every function was audited for bad inputs, missing inputs, and unexpected values. All findings fixed
|
|
922
|
+
- **No runtime dependencies** — the bundle is the entire library
|
|
923
|
+
|
|
924
|
+
```bash
|
|
925
|
+
npm test # full suite + coverage
|
|
926
|
+
npm run test:cet # Europe/Berlin (CET/CEST) clock change suite
|
|
927
|
+
npm run test:ny # America/New_York (EST/EDT) clock change suite
|
|
928
|
+
npm run test:la # America/Los_Angeles — date-only west-of-UTC guard
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## License
|
|
934
|
+
|
|
935
|
+
[MIT](LICENSE) © 2026 Luke Price (The Purple Brock).
|