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 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
- # Temporary Holding Version
1
+ # duration
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }