@williamthorsen/toolbelt.datetime 3.2.8 → 4.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 +59 -0
- package/README.md +9 -1
- package/dist/esm/1-proposed/formatDuration.d.ts +4 -0
- package/dist/esm/1-proposed/formatDuration.js +48 -0
- package/dist/esm/1-proposed/index.d.ts +4 -2
- package/dist/esm/1-proposed/index.js +4 -2
- package/dist/esm/1-proposed/listDecadesContainingRange.d.ts +5 -0
- package/dist/esm/1-proposed/{getDecadesContainingRange.js → listDecadesContainingRange.js} +1 -1
- package/dist/esm/1-proposed/{getDecadesContainingYears.d.ts → listDecadesContainingYears.d.ts} +1 -1
- package/dist/esm/1-proposed/{getDecadesContainingYears.js → listDecadesContainingYears.js} +2 -2
- package/dist/esm/1-proposed/startTimer.d.ts +1 -0
- package/dist/esm/1-proposed/startTimer.js +4 -0
- package/dist/esm/1-proposed/types.d.ts +1 -1
- package/dist/esm/2-draft/TimeUnit.d.ts +3 -2
- package/dist/esm/2-draft/TimeUnit.js +16 -8
- package/dist/esm/2-draft/Timestamp.d.ts +3 -3
- package/dist/esm/2-draft/Timestamp.js +11 -11
- package/package.json +1 -1
- package/dist/esm/1-proposed/getDecadesContainingRange.d.ts +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 4.0.0 — 2026-08-12
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- 🚨 **Breaking:** Rename get* functions by return kind and verb specificity (#119)
|
|
10
|
+
|
|
11
|
+
Renames thirteen functions across various packages to align with a consistent naming pattern.
|
|
12
|
+
|
|
13
|
+
### Refactoring
|
|
14
|
+
|
|
15
|
+
- Align stray modules with layout and TypeScript conventions (#118)
|
|
16
|
+
|
|
17
|
+
Aligns all packages with code-layout and annotation conventions, ending a handful of long-standing exceptions. Documentation has been updated to make the conventions clear.
|
|
18
|
+
|
|
19
|
+
## 3.3.0 — 2026-08-08
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
- Add elapsed-time measurement and multi-unit duration formatting (#95)
|
|
24
|
+
|
|
25
|
+
Adds two functions for measuring and formatting durations to `@williamthorsen/toolbelt.datetime` at the "proposed" stage. `startTimer` measures how long a span of work takes, returning a reader that reports whole elapsed milliseconds and can be read repeatedly during the span; it measures independently of the system clock, so a clock adjustment partway through does not skew the result. `formatDuration` renders a millisecond count as a short labeled duration and picks the unit itself, so 240,000 becomes `4m`; an option raises the ceiling on how many components appear, so 250,300 becomes `4m 10s 300ms`.
|
|
26
|
+
|
|
27
|
+
`TimeUnit`, in `@williamthorsen/toolbelt.datetime/draft`, now converts to a coarser unit exactly: an hour expressed in milliseconds converts to one hour, where it previously came back a fraction short and could truncate to zero. `TimeUnit` also now exposes its units as a list ordered from coarsest to finest.
|
|
28
|
+
|
|
29
|
+
- Use underscore separator at 4 digits or more
|
|
30
|
+
|
|
31
|
+
Changes the `unicorn/numeric-separators-style` rule config so that separators are consistently used in base 10 numbers, instead of exempting numbers of 5 digits or less.
|
|
32
|
+
|
|
33
|
+
### Refactoring
|
|
34
|
+
|
|
35
|
+
- Fixes violations surfaced by newly active lint rules (#84)
|
|
36
|
+
- Fix slug punctuation and require safe integers (#86)
|
|
37
|
+
|
|
38
|
+
- Fixes an issue where the use of certain letters as the slug separator in `slugify` would leave punctuation marks in the result.
|
|
39
|
+
- Time-unit conversions, scaling range bounds, normal-distribution interval counts, and array indices in object paths now reject values too large to represent exactly instead of silently losing precision.
|
|
40
|
+
- Seeded number generators now produce distinct sequences for seeds at or beyond 2^53, where adjacent seeds previously collapsed onto nearly identical output. A seed of that size saved before this release no longer reproduces the same output.
|
|
41
|
+
|
|
42
|
+
- Fix deferred violations of unicorn lint rules (#88)
|
|
43
|
+
|
|
44
|
+
Fixes issues surfaced by `unicorn` lint rules that were temporarily downgraded to warnings and restores the rules to "error" severity. Separately, the `Queue` class in `dstructs` has been modified to hide its internal array.
|
|
45
|
+
|
|
46
|
+
### Tooling
|
|
47
|
+
|
|
48
|
+
- Migrate Vitest configs to the nmr projects model (#73)
|
|
49
|
+
|
|
50
|
+
Packages no longer need to declare their own Vitest config. Test suites are now selected by a test file's name suffix rather than by choosing a config file: `*.app.test.ts` and `*.int.test.ts` route to the app and integration suites, and everything else runs as a unit test. Local development is now declared to require Node 24.16 or later.
|
|
51
|
+
|
|
52
|
+
- Adopt the mechanical-syntax deferred unicorn rules (#82)
|
|
53
|
+
|
|
54
|
+
Promotes ten deferred lint rules from warnings to errors and fixes the issues surfaced by those rules. One published behavior changes as well: Converting a Map to a plain object no longer drops an entry keyed `__proto__`.
|
|
55
|
+
|
|
56
|
+
- Use identical compiler settings for all packages (#105)
|
|
57
|
+
|
|
58
|
+
All packages now have identical compiler settings, using the settings from the `@williamthorsen/tsconfig` base config without modification.
|
|
59
|
+
|
|
60
|
+
### Dependencies
|
|
61
|
+
|
|
62
|
+
- Upgrade all deps to latest version
|
|
63
|
+
|
|
5
64
|
## 3.2.8 — 2026-07-27
|
|
6
65
|
|
|
7
66
|
### Tooling
|
package/README.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Date and time utilities.
|
|
4
4
|
|
|
5
|
-
<!-- section:release-notes
|
|
5
|
+
<!-- section:release-notes -->
|
|
6
|
+
## Release notes — v4.0.0 (2026-08-12)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- 🚨 **Breaking:** Rename get* functions by return kind and verb specificity (#119)
|
|
11
|
+
|
|
12
|
+
Renames thirteen functions across various packages to align with a consistent naming pattern.
|
|
13
|
+
<!-- /section:release-notes -->
|
|
6
14
|
|
|
7
15
|
## Installation
|
|
8
16
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { TimeUnit } from "../2-draft/index.js";
|
|
2
|
+
const FINEST_INDEX = TimeUnit.coarsestFirst.length - 1;
|
|
3
|
+
export function formatDuration(milliseconds, options = {}) {
|
|
4
|
+
const { maxUnits = 1 } = options;
|
|
5
|
+
assertValidArguments(milliseconds, maxUnits);
|
|
6
|
+
const components = selectComponents(milliseconds, maxUnits);
|
|
7
|
+
return components.map(({ count, unit }) => unit.formatLabeledCount(count, { format: 'short' })).join(' ');
|
|
8
|
+
}
|
|
9
|
+
function selectComponents(milliseconds, maxUnits) {
|
|
10
|
+
let leadingIndex = selectLeadingIndex(milliseconds);
|
|
11
|
+
for (;;) {
|
|
12
|
+
const shownUnits = TimeUnit.coarsestFirst.slice(leadingIndex, leadingIndex + maxUnits);
|
|
13
|
+
const rounded = roundToFinest(milliseconds, shownUnits);
|
|
14
|
+
const carriedIndex = selectLeadingIndex(rounded);
|
|
15
|
+
if (carriedIndex >= leadingIndex) {
|
|
16
|
+
return decompose(rounded, shownUnits);
|
|
17
|
+
}
|
|
18
|
+
leadingIndex = carriedIndex;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function selectLeadingIndex(milliseconds) {
|
|
22
|
+
const index = TimeUnit.coarsestFirst.findIndex((unit) => milliseconds >= unit.inMillis);
|
|
23
|
+
return index === -1 ? FINEST_INDEX : index;
|
|
24
|
+
}
|
|
25
|
+
function roundToFinest(milliseconds, shownUnits) {
|
|
26
|
+
const finestInMillis = Math.min(...shownUnits.map((unit) => unit.inMillis));
|
|
27
|
+
return Math.round(milliseconds / finestInMillis) * finestInMillis;
|
|
28
|
+
}
|
|
29
|
+
function decompose(milliseconds, shownUnits) {
|
|
30
|
+
const components = [];
|
|
31
|
+
let remainder = milliseconds;
|
|
32
|
+
for (const [index, unit] of shownUnits.entries()) {
|
|
33
|
+
const count = Math.floor(remainder / unit.inMillis);
|
|
34
|
+
remainder -= count * unit.inMillis;
|
|
35
|
+
if (index === 0 || count > 0) {
|
|
36
|
+
components.push({ count, unit });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return components;
|
|
40
|
+
}
|
|
41
|
+
function assertValidArguments(milliseconds, maxUnits) {
|
|
42
|
+
if (!Number.isFinite(milliseconds) || milliseconds < 0) {
|
|
43
|
+
throw new RangeError(`Duration must be a non-negative finite number of milliseconds, but was ${milliseconds}.`);
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isSafeInteger(maxUnits) || maxUnits < 1) {
|
|
46
|
+
throw new RangeError(`maxUnits must be a positive integer, but was ${maxUnits}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { formatDuration, type FormatDurationOptions } from './formatDuration.js';
|
|
2
|
+
export { listDecadesContainingRange } from './listDecadesContainingRange.js';
|
|
3
|
+
export { listDecadesContainingYears } from './listDecadesContainingYears.js';
|
|
4
|
+
export { startTimer } from './startTimer.js';
|
|
3
5
|
export * from './types.js';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { formatDuration } from "./formatDuration.js";
|
|
2
|
+
export { listDecadesContainingRange } from "./listDecadesContainingRange.js";
|
|
3
|
+
export { listDecadesContainingYears } from "./listDecadesContainingYears.js";
|
|
4
|
+
export { startTimer } from "./startTimer.js";
|
|
3
5
|
export * from "./types.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function listDecadesContainingYears(years) {
|
|
2
2
|
const uniqueYears = [...new Set(years)].toSorted((a, b) => a - b);
|
|
3
3
|
const decadesMap = new Map();
|
|
4
4
|
const [firstYear] = uniqueYears;
|
|
@@ -18,5 +18,5 @@ export function getDecadesContainingYears(years) {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
return
|
|
21
|
+
return decadesMap.values().toArray();
|
|
22
22
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startTimer(): () => number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { Decade } from './
|
|
1
|
+
export type { Decade } from './listDecadesContainingYears.js';
|
|
@@ -5,13 +5,14 @@ export declare class TimeUnit {
|
|
|
5
5
|
static readonly Minutes: TimeUnit;
|
|
6
6
|
static readonly Hours: TimeUnit;
|
|
7
7
|
static readonly Days: TimeUnit;
|
|
8
|
+
static readonly coarsestFirst: ReadonlyArray<TimeUnit>;
|
|
8
9
|
readonly abbrev: string;
|
|
9
10
|
readonly plural: string;
|
|
10
11
|
readonly singular: string;
|
|
11
12
|
private constructor();
|
|
12
13
|
static convert(amount: number, fromUnit: TimeUnit, toUnit: TimeUnit, options?: TimeUnitConversionOptions): number;
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
formatLabeledCount(amount: number, options?: TimeUnitLabelOptions): string;
|
|
15
|
+
inflectLabel(amount: number): string;
|
|
15
16
|
toString(): string;
|
|
16
17
|
}
|
|
17
18
|
export interface TimeUnitConversionOptions {
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
export class TimeUnit {
|
|
2
2
|
inMillis;
|
|
3
3
|
static Millis = new TimeUnit(1, { singular: 'millisecond', abbrev: 'ms' });
|
|
4
|
-
static Seconds = new TimeUnit(
|
|
4
|
+
static Seconds = new TimeUnit(1_000, { singular: 'second', abbrev: 's' });
|
|
5
5
|
static Minutes = new TimeUnit(60_000, { singular: 'minute', abbrev: 'm' });
|
|
6
6
|
static Hours = new TimeUnit(3_600_000, { singular: 'hour', abbrev: 'h' });
|
|
7
7
|
static Days = new TimeUnit(86_400_000, { singular: 'day', abbrev: 'd' });
|
|
8
|
+
static coarsestFirst = [
|
|
9
|
+
TimeUnit.Days,
|
|
10
|
+
TimeUnit.Hours,
|
|
11
|
+
TimeUnit.Minutes,
|
|
12
|
+
TimeUnit.Seconds,
|
|
13
|
+
TimeUnit.Millis,
|
|
14
|
+
];
|
|
8
15
|
abbrev;
|
|
9
16
|
plural;
|
|
10
17
|
singular;
|
|
@@ -19,23 +26,24 @@ export class TimeUnit {
|
|
|
19
26
|
if (fromUnit.inMillis === toUnit.inMillis) {
|
|
20
27
|
return amount;
|
|
21
28
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const value = fromUnit.inMillis > toUnit.inMillis
|
|
30
|
+
? amount * (fromUnit.inMillis / toUnit.inMillis)
|
|
31
|
+
: amount / (toUnit.inMillis / fromUnit.inMillis);
|
|
32
|
+
if (throwOnFractional && !Number.isSafeInteger(value)) {
|
|
33
|
+
throw new Error(`${fromUnit.formatLabeledCount(amount)} cannot be converted into an exact whole number of ${toUnit.plural}.`);
|
|
26
34
|
}
|
|
27
35
|
if (decimalPlaces !== undefined) {
|
|
28
36
|
return Math.round(value * 10 ** decimalPlaces) / 10 ** decimalPlaces;
|
|
29
37
|
}
|
|
30
38
|
return value;
|
|
31
39
|
}
|
|
32
|
-
|
|
40
|
+
formatLabeledCount(amount, options = {}) {
|
|
33
41
|
if (options.format === 'short') {
|
|
34
42
|
return `${amount}${this.abbrev}`;
|
|
35
43
|
}
|
|
36
|
-
return `${amount} ${this.
|
|
44
|
+
return `${amount} ${this.inflectLabel(amount)}`;
|
|
37
45
|
}
|
|
38
|
-
|
|
46
|
+
inflectLabel(amount) {
|
|
39
47
|
return amount === 1 ? this.singular : this.plural;
|
|
40
48
|
}
|
|
41
49
|
toString() {
|
|
@@ -3,14 +3,14 @@ export declare class Timestamp {
|
|
|
3
3
|
private _format;
|
|
4
4
|
private readonly _millis;
|
|
5
5
|
private _timeUnit;
|
|
6
|
-
static fromMillis(milliseconds: number): Timestamp;
|
|
7
|
-
static fromSeconds(seconds: number): Timestamp;
|
|
8
|
-
static now(options?: TimestampOptions): Timestamp;
|
|
9
6
|
constructor(dateTime?: TimestampInput, options?: TimestampOptions);
|
|
10
7
|
get format(): TimestampFormatEnum;
|
|
11
8
|
get millis(): number;
|
|
12
9
|
get seconds(): number;
|
|
13
10
|
get timeUnit(): TimeUnit;
|
|
11
|
+
static fromMillis(milliseconds: number): Timestamp;
|
|
12
|
+
static fromSeconds(seconds: number): Timestamp;
|
|
13
|
+
static now(options?: TimestampOptions): Timestamp;
|
|
14
14
|
clone(options?: TimestampOptions): Timestamp;
|
|
15
15
|
setOptions(options?: TimestampOptions): this;
|
|
16
16
|
toCompactString(options?: Options): string;
|
|
@@ -3,15 +3,6 @@ export class Timestamp {
|
|
|
3
3
|
_format;
|
|
4
4
|
_millis;
|
|
5
5
|
_timeUnit;
|
|
6
|
-
static fromMillis(milliseconds) {
|
|
7
|
-
return new Timestamp(new Date(milliseconds));
|
|
8
|
-
}
|
|
9
|
-
static fromSeconds(seconds) {
|
|
10
|
-
return new Timestamp(new Date(seconds * 1000), { timeUnit: TimeUnit.Seconds });
|
|
11
|
-
}
|
|
12
|
-
static now(options = {}) {
|
|
13
|
-
return new Timestamp(undefined, options);
|
|
14
|
-
}
|
|
15
6
|
constructor(dateTime, options = {}) {
|
|
16
7
|
const { format = 'iso', timeUnit = TimeUnit.Millis } = options;
|
|
17
8
|
if (dateTime instanceof Timestamp) {
|
|
@@ -38,6 +29,15 @@ export class Timestamp {
|
|
|
38
29
|
get timeUnit() {
|
|
39
30
|
return this._timeUnit;
|
|
40
31
|
}
|
|
32
|
+
static fromMillis(milliseconds) {
|
|
33
|
+
return new Timestamp(new Date(milliseconds));
|
|
34
|
+
}
|
|
35
|
+
static fromSeconds(seconds) {
|
|
36
|
+
return new Timestamp(new Date(seconds * 1_000), { timeUnit: TimeUnit.Seconds });
|
|
37
|
+
}
|
|
38
|
+
static now(options = {}) {
|
|
39
|
+
return new Timestamp(undefined, options);
|
|
40
|
+
}
|
|
41
41
|
clone(options = {}) {
|
|
42
42
|
return new Timestamp(this._millis, options);
|
|
43
43
|
}
|
|
@@ -48,7 +48,7 @@ export class Timestamp {
|
|
|
48
48
|
return this;
|
|
49
49
|
}
|
|
50
50
|
toCompactString(options = {}) {
|
|
51
|
-
const [datePart = '', timePart = ''] = this.toIsoString(options).split('T');
|
|
51
|
+
const [datePart = '', timePart = ''] = this.toIsoString(options).split('T', 2);
|
|
52
52
|
const formattedDatePart = datePart.replaceAll(/[-Z]/g, '');
|
|
53
53
|
const formattedTimePart = timePart.replaceAll(/[:Z]/g, '');
|
|
54
54
|
return timePart.length > 0 ? `${formattedDatePart}-${formattedTimePart}` : formattedDatePart;
|
|
@@ -75,7 +75,7 @@ export class Timestamp {
|
|
|
75
75
|
case TimeUnit.Hours:
|
|
76
76
|
return isoDateTime.replace(/:\d{2}:\d{2}\.\d{3}Z$/, 'Z');
|
|
77
77
|
case TimeUnit.Days: {
|
|
78
|
-
const [datePart] = isoDateTime.split('T');
|
|
78
|
+
const [datePart] = isoDateTime.split('T', 1);
|
|
79
79
|
return `${datePart}Z`;
|
|
80
80
|
}
|
|
81
81
|
case TimeUnit.Millis:
|
package/package.json
CHANGED