z-duration 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 +148 -2
- package/dist/index.d.ts +23 -0
- package/dist/index.js +47 -0
- package/package.json +47 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zachary Lysobey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,149 @@
|
|
|
1
|
-
#
|
|
1
|
+
# duration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A small, dependency-light TypeScript library for representing, converting,
|
|
4
|
+
and normalizing fixed durations. It builds on
|
|
5
|
+
[`time-units`](../time-units), reusing its scalar unit-conversion
|
|
6
|
+
constants and functions rather than duplicating them.
|
|
7
|
+
|
|
8
|
+
The entire implementation is a single TypeScript file:
|
|
9
|
+
[`index.ts`](https://github.com/zachlysobey/z-megarepo/blob/master/duration/index.ts).
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
balanceDuration,
|
|
16
|
+
durationToHours,
|
|
17
|
+
durationToMilliseconds,
|
|
18
|
+
hoursToDuration,
|
|
19
|
+
millisecondsToDuration,
|
|
20
|
+
} from "z-duration";
|
|
21
|
+
|
|
22
|
+
durationToMilliseconds({ minutes: 1, seconds: 30 }); // 90_000
|
|
23
|
+
durationToHours({ days: 1, hours: 12 }); // 36
|
|
24
|
+
|
|
25
|
+
millisecondsToDuration(90_000); // { minutes: 1, seconds: 30 }
|
|
26
|
+
hoursToDuration(49); // { days: 2, hours: 1 }
|
|
27
|
+
|
|
28
|
+
balanceDuration({ days: 3, hours: 48 }); // { days: 5 }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The `Duration` type
|
|
32
|
+
|
|
33
|
+
A `Duration` is a plain object with one or more of the supported unit
|
|
34
|
+
fields: `milliseconds`, `seconds`, `minutes`, `hours`, `days`. It must
|
|
35
|
+
contain at least one field, and it is not required to be balanced:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const a: Duration = { days: 3 };
|
|
39
|
+
const b: Duration = { hours: 2, minutes: 30 };
|
|
40
|
+
const c: Duration = { days: 3, hours: 48 }; // valid, unbalanced
|
|
41
|
+
const invalid: Duration = {}; // type error
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Field values are ordinary `number`s: fractional and negative values are
|
|
45
|
+
valid and are never rounded, rejected, or coerced.
|
|
46
|
+
|
|
47
|
+
A day always means exactly 24 elapsed hours — this package has no
|
|
48
|
+
calendar, timezone, or date awareness (see [Package boundaries](#package-boundaries)).
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### Duration to scalar
|
|
53
|
+
|
|
54
|
+
Each function reduces a full, possibly-unbalanced `Duration` into a
|
|
55
|
+
single scalar unit. Missing fields count as zero.
|
|
56
|
+
|
|
57
|
+
- `durationToMilliseconds(duration)`
|
|
58
|
+
- `durationToSeconds(duration)`
|
|
59
|
+
- `durationToMinutes(duration)`
|
|
60
|
+
- `durationToHours(duration)`
|
|
61
|
+
- `durationToDays(duration)`
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
durationToDays({ days: 3, hours: 48 }); // 5
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Scalar to Duration
|
|
68
|
+
|
|
69
|
+
Each function converts a scalar quantity into a balanced `Duration`,
|
|
70
|
+
expressed using the largest appropriate units, with any remainder carried
|
|
71
|
+
into smaller units rather than rounded away.
|
|
72
|
+
|
|
73
|
+
- `millisecondsToDuration(milliseconds)`
|
|
74
|
+
- `secondsToDuration(seconds)`
|
|
75
|
+
- `minutesToDuration(minutes)`
|
|
76
|
+
- `hoursToDuration(hours)`
|
|
77
|
+
- `daysToDuration(days)`
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
hoursToDuration(1.5); // { hours: 1, minutes: 30 }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `balanceDuration(duration)`
|
|
84
|
+
|
|
85
|
+
Normalizes an arbitrary, possibly-unbalanced `Duration` into canonical
|
|
86
|
+
form: the largest appropriate units, zero-valued fields omitted.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
balanceDuration({ minutes: 90, seconds: 30 });
|
|
90
|
+
// { hours: 1, minutes: 30, seconds: 30 }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`balanceDuration` is normalization, not rounding — it never discards
|
|
94
|
+
part of the represented value.
|
|
95
|
+
|
|
96
|
+
## Semantics
|
|
97
|
+
|
|
98
|
+
- **Balancing order**: days → hours → minutes → seconds → milliseconds,
|
|
99
|
+
using the fixed relationships from `time-units` (1 day = 24 hours,
|
|
100
|
+
1 hour = 60 minutes, 1 minute = 60 seconds, 1 second = 1000
|
|
101
|
+
milliseconds).
|
|
102
|
+
- **Zero-valued fields are omitted** from a balanced result, e.g.
|
|
103
|
+
`hoursToDuration(24)` returns `{ days: 1 }`, not
|
|
104
|
+
`{ days: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }`. The
|
|
105
|
+
one exception is a duration that nets to exactly zero, which is
|
|
106
|
+
reported as `{ milliseconds: 0 }` so the result still satisfies the
|
|
107
|
+
`Duration` type's at-least-one-field requirement.
|
|
108
|
+
- **Negative durations** are valid and unrestricted. Balancing truncates
|
|
109
|
+
toward zero at each unit, so every field in a balanced result shares
|
|
110
|
+
the same sign — e.g. `minutesToDuration(-1.5)` returns
|
|
111
|
+
`{ minutes: -1, seconds: -30 }`, never a duration whose fields disagree
|
|
112
|
+
in sign or that changes the represented value.
|
|
113
|
+
- **Fractional values** are valid and are never rounded. Any remainder
|
|
114
|
+
that can't be expressed in a larger unit is carried down to smaller
|
|
115
|
+
units, and a remainder too small even for milliseconds is left exactly
|
|
116
|
+
as ordinary JavaScript floating-point arithmetic produces it.
|
|
117
|
+
|
|
118
|
+
## Package boundaries
|
|
119
|
+
|
|
120
|
+
`duration` represents and transforms fixed elapsed-time durations. It
|
|
121
|
+
does not know about dates, timestamps, timezones, or calendars, and it
|
|
122
|
+
has no parsing, formatting, or rounding policy. Operations like
|
|
123
|
+
`addDuration(duration)(date)`, `nextMonday(...)`, `parseDuration(...)`,
|
|
124
|
+
`formatDuration(...)`, or `roundDuration(...)` belong in a separate,
|
|
125
|
+
higher-level package.
|
|
126
|
+
|
|
127
|
+
## Mutability
|
|
128
|
+
|
|
129
|
+
All operations are pure: no argument is mutated, and every function
|
|
130
|
+
returns a new value.
|
|
131
|
+
|
|
132
|
+
## Contributing
|
|
133
|
+
|
|
134
|
+
See the
|
|
135
|
+
[CONTRIBUTING guide](https://github.com/zachlysobey/z-megarepo/blob/master/duration/docs/CONTRIBUTING.md)
|
|
136
|
+
for setup and development scripts.
|
|
137
|
+
|
|
138
|
+
## Packaging
|
|
139
|
+
|
|
140
|
+
Published as ESM-only with bundled type declarations, built into `dist/`
|
|
141
|
+
by `npm run build`. Runtime-agnostic (browsers, Node, Bun, Deno,
|
|
142
|
+
workers), with `sideEffects: false` so bundlers can tree-shake unused
|
|
143
|
+
exports.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
[MIT](https://github.com/zachlysobey/z-megarepo/blob/master/duration/LICENSE)
|
|
148
|
+
— unlike the rest of the surrounding monorepo, which is GPL-3.0-or-later,
|
|
149
|
+
this package is MIT-licensed to encourage adoption.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Requires at least one property of `T` to be present. */
|
|
2
|
+
type RequireAtLeastOne<T> = {
|
|
3
|
+
[K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>>;
|
|
4
|
+
}[keyof T];
|
|
5
|
+
export type Duration = RequireAtLeastOne<{
|
|
6
|
+
milliseconds: number;
|
|
7
|
+
seconds: number;
|
|
8
|
+
minutes: number;
|
|
9
|
+
hours: number;
|
|
10
|
+
days: number;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const durationToMilliseconds: (duration: Duration) => number;
|
|
13
|
+
export declare const durationToSeconds: (duration: Duration) => number;
|
|
14
|
+
export declare const durationToMinutes: (duration: Duration) => number;
|
|
15
|
+
export declare const durationToHours: (duration: Duration) => number;
|
|
16
|
+
export declare const durationToDays: (duration: Duration) => number;
|
|
17
|
+
export declare const millisecondsToDuration: (milliseconds: number) => Duration;
|
|
18
|
+
export declare const secondsToDuration: (seconds: number) => Duration;
|
|
19
|
+
export declare const minutesToDuration: (minutes: number) => Duration;
|
|
20
|
+
export declare const hoursToDuration: (hours: number) => Duration;
|
|
21
|
+
export declare const daysToDuration: (days: number) => Duration;
|
|
22
|
+
export declare const balanceDuration: (duration: Duration) => Duration;
|
|
23
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MILLISECONDS_PER_SECOND, MILLISECONDS_PER_MINUTE, MILLISECONDS_PER_HOUR, MILLISECONDS_PER_DAY, daysToMilliseconds, hoursToMilliseconds, minutesToMilliseconds, secondsToMilliseconds, millisecondsToSeconds, millisecondsToMinutes, millisecondsToHours, millisecondsToDays, } from 'z-time-units';
|
|
2
|
+
export const durationToMilliseconds = (duration) => daysToMilliseconds(duration.days ?? 0) +
|
|
3
|
+
hoursToMilliseconds(duration.hours ?? 0) +
|
|
4
|
+
minutesToMilliseconds(duration.minutes ?? 0) +
|
|
5
|
+
secondsToMilliseconds(duration.seconds ?? 0) +
|
|
6
|
+
(duration.milliseconds ?? 0);
|
|
7
|
+
export const durationToSeconds = (duration) => millisecondsToSeconds(durationToMilliseconds(duration));
|
|
8
|
+
export const durationToMinutes = (duration) => millisecondsToMinutes(durationToMilliseconds(duration));
|
|
9
|
+
export const durationToHours = (duration) => millisecondsToHours(durationToMilliseconds(duration));
|
|
10
|
+
export const durationToDays = (duration) => millisecondsToDays(durationToMilliseconds(duration));
|
|
11
|
+
export const millisecondsToDuration = (milliseconds) => {
|
|
12
|
+
const result = {};
|
|
13
|
+
let remaining = milliseconds;
|
|
14
|
+
const days = Math.trunc(remaining / MILLISECONDS_PER_DAY);
|
|
15
|
+
if (days !== 0) {
|
|
16
|
+
result.days = days;
|
|
17
|
+
remaining -= days * MILLISECONDS_PER_DAY;
|
|
18
|
+
}
|
|
19
|
+
const hours = Math.trunc(remaining / MILLISECONDS_PER_HOUR);
|
|
20
|
+
if (hours !== 0) {
|
|
21
|
+
result.hours = hours;
|
|
22
|
+
remaining -= hours * MILLISECONDS_PER_HOUR;
|
|
23
|
+
}
|
|
24
|
+
const minutes = Math.trunc(remaining / MILLISECONDS_PER_MINUTE);
|
|
25
|
+
if (minutes !== 0) {
|
|
26
|
+
result.minutes = minutes;
|
|
27
|
+
remaining -= minutes * MILLISECONDS_PER_MINUTE;
|
|
28
|
+
}
|
|
29
|
+
const seconds = Math.trunc(remaining / MILLISECONDS_PER_SECOND);
|
|
30
|
+
if (seconds !== 0) {
|
|
31
|
+
result.seconds = seconds;
|
|
32
|
+
remaining -= seconds * MILLISECONDS_PER_SECOND;
|
|
33
|
+
}
|
|
34
|
+
// `remaining` may be a legitimate 0, or -0 from truncating a negative
|
|
35
|
+
// fraction; `|| 0` collapses -0 to 0 without disturbing any other value.
|
|
36
|
+
// At least one field is required, so an all-zero result still reports
|
|
37
|
+
// `{ milliseconds: 0 }` rather than an empty object.
|
|
38
|
+
if (remaining !== 0 || Object.keys(result).length === 0) {
|
|
39
|
+
result.milliseconds = remaining || 0;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
export const secondsToDuration = (seconds) => millisecondsToDuration(secondsToMilliseconds(seconds));
|
|
44
|
+
export const minutesToDuration = (minutes) => millisecondsToDuration(minutesToMilliseconds(minutes));
|
|
45
|
+
export const hoursToDuration = (hours) => millisecondsToDuration(hoursToMilliseconds(hours));
|
|
46
|
+
export const daysToDuration = (days) => millisecondsToDuration(daysToMilliseconds(days));
|
|
47
|
+
export const balanceDuration = (duration) => millisecondsToDuration(durationToMilliseconds(duration));
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-duration",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tiny TypeScript library for representing, converting, and normalizing fixed durations",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test \"*.test.ts\"",
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepack": "npm run build"
|
|
10
|
+
},
|
|
11
|
+
"author": "Zachary Lysobey <zach@lysobey.com> (https://github.com/zachlysobey)",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22",
|
|
16
|
+
"npm": ">=10"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"z-time-units": "^1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.13.3",
|
|
23
|
+
"typescript": "^7.0.2"
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/index.js",
|
|
28
|
+
"dist/index.d.ts"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"devEngines": {
|
|
38
|
+
"runtime": {
|
|
39
|
+
"name": "node",
|
|
40
|
+
"version": ">=24 <25",
|
|
41
|
+
"onFail": "error"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/zachlysobey/z-megarepo.git",
|
|
47
|
+
"directory": "duration"
|
|
48
|
+
}
|
|
49
|
+
}
|