nanoduration 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/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # Duration
2
+
3
+ An immutable, non-negative, Rust-inspired `Duration` type for JavaScript and TypeScript.
4
+
5
+ This library provides a small, predictable abstraction for representing spans of time with explicit units, saturating arithmetic, and strong invariants—without calendars, time zones, or parsing logic.
6
+
7
+ ---
8
+
9
+ ## Summary
10
+
11
+ `Duration` is a value-type abstraction for time intervals, modeled after Rust’s `std::time::Duration`.
12
+
13
+ It is designed for:
14
+
15
+ - timeouts
16
+ - delays
17
+ - retry/backoff logic
18
+ - infrastructure and systems-oriented code
19
+
20
+ The library prioritizes correctness, explicitness, and simplicity over feature breadth.
21
+
22
+ ---
23
+
24
+ ## What this library does
25
+
26
+ - Represents time as an immutable value (internally stored in nanoseconds)
27
+ - Enforces **non-negative durations**
28
+ - Provides **explicit unit constructors** (`seconds`, `milliseconds`, etc.)
29
+ - Supports **saturating arithmetic** (durations never go negative)
30
+ - Avoids all calendar, timezone, and locale concerns
31
+ - Ships with **full TypeScript type definitions**
32
+
33
+ ### What it intentionally does not do
34
+
35
+ - Date/time manipulation
36
+ - Parsing human-readable strings
37
+ - Time zones or calendars
38
+ - Implicit unit coercion
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```sh
45
+ npm install nanoduration
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Usage
51
+
52
+ ### Creating durations
53
+
54
+ ```ts
55
+ import { Duration } from "nanoduration";
56
+
57
+ const a = Duration.fromSecs(2);
58
+ const b = Duration.fromMillis(500);
59
+ ```
60
+
61
+ ### Arithmetic
62
+
63
+ ```ts
64
+ const total = a.add(b); // 2.5 seconds
65
+ const remaining = total.sub(Duration.fromSecs(5)); // saturates to 0
66
+ ```
67
+
68
+ ### Conversions
69
+
70
+ ```ts
71
+ total.asMillis(); // 2500
72
+ total.asSecs(); // 2.5
73
+ ```
74
+
75
+ ### Comparisons
76
+
77
+ ```ts
78
+ total.gt(Duration.fromSecs(2)); // true
79
+ total.eq(Duration.fromMillis(2500)); // true
80
+ ```
81
+
82
+ ### Utilities
83
+
84
+ ```ts
85
+ Duration.ZERO.isZero(); // true
86
+
87
+ const clamped = total.clamp(Duration.fromSecs(1), Duration.fromSecs(3));
88
+ ```
89
+
90
+ ### Interop with Node.js APIs
91
+
92
+ ```ts
93
+ setTimeout(() => {
94
+ // ...
95
+ }, total.asMillis());
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Benefits of using this library
101
+
102
+ ### Predictable semantics
103
+
104
+ - Durations never become negative
105
+ - No silent unit conversions
106
+ - No hidden calendar logic
107
+
108
+ ### Safer than raw numbers
109
+
110
+ - Units are explicit at construction
111
+ - Arithmetic behavior is well-defined
112
+ - Fewer “milliseconds vs seconds” bugs
113
+
114
+ ### Lightweight
115
+
116
+ - No dependencies
117
+ - Small API surface
118
+ - Tree-shakable
119
+ - Minimal runtime overhead
120
+
121
+ ### TypeScript-first
122
+
123
+ - Strict typings included
124
+ - Works out of the box with modern ESM setups
125
+
126
+ ---
127
+
128
+ ## Design notes
129
+
130
+ - Internal unit: **nanoseconds**
131
+ - Arithmetic: **saturating**
132
+ - Mutability: **immutable**
133
+ - Module format: **ESM**
134
+ - Inspired by: Rust’s `std::time::Duration`
135
+
136
+ ---
137
+
138
+ ## Contributing
139
+
140
+ Contributions are welcome.
141
+
142
+ ### Guidelines
143
+
144
+ 1. Keep the API minimal and explicit
145
+ 2. Maintain non-negative duration invariants
146
+ 3. Preserve backward compatibility where possible
147
+ 4. Run `npm run build` before submitting changes
148
+
149
+ ## License
150
+
151
+ MIT License.
152
+
153
+ See the `LICENSE` file for details.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Represents an immutable, non-negative span of time.
3
+ *
4
+ * ## Design characteristics
5
+ * - Internally stored as nanoseconds
6
+ * - Saturating arithmetic (never becomes negative)
7
+ * - No calendar, timezone, or locale semantics
8
+ * - Explicit unit construction and observation
9
+ *
10
+ * This type is intended for:
11
+ * - Timeouts
12
+ * - Intervals
13
+ * - Backoff calculations
14
+ * - System and infrastructure code
15
+ */
16
+ export declare class Duration {
17
+ /** Nanoseconds per microsecond */
18
+ private static readonly NS_PER_MICRO;
19
+ /** Nanoseconds per millisecond */
20
+ private static readonly NS_PER_MILLI;
21
+ /** Nanoseconds per second */
22
+ private static readonly NS_PER_SECOND;
23
+ /** Nanoseconds per minute */
24
+ private static readonly NS_PER_MINUTE;
25
+ /** Nanoseconds per hour */
26
+ private static readonly NS_PER_HOUR;
27
+ /**
28
+ * A duration of zero length.
29
+ */
30
+ static readonly ZERO: Duration;
31
+ private readonly nanoseconds;
32
+ /**
33
+ * Internal constructor.
34
+ *
35
+ * All instances are guaranteed to contain a non-negative,
36
+ * finite number of nanoseconds.
37
+ */
38
+ private constructor();
39
+ /**
40
+ * Creates a duration from nanoseconds.
41
+ *
42
+ * @param nanos - Number of nanoseconds
43
+ */
44
+ static fromNanos(nanos: number): Duration;
45
+ /**
46
+ * Creates a duration from microseconds.
47
+ *
48
+ * @param micros - Number of microseconds
49
+ */
50
+ static fromMicros(micros: number): Duration;
51
+ /**
52
+ * Creates a duration from milliseconds.
53
+ *
54
+ * @param millis - Number of milliseconds
55
+ */
56
+ static fromMillis(millis: number): Duration;
57
+ /**
58
+ * Creates a duration from seconds.
59
+ *
60
+ * @param secs - Number of seconds
61
+ */
62
+ static fromSecs(secs: number): Duration;
63
+ /**
64
+ * Creates a duration from minutes.
65
+ *
66
+ * @param minutes - Number of minutes
67
+ */
68
+ static fromMinutes(minutes: number): Duration;
69
+ /**
70
+ * Creates a duration from hours.
71
+ *
72
+ * @param hours - Number of hours
73
+ */
74
+ static fromHours(hours: number): Duration;
75
+ /**
76
+ * Returns the total duration in nanoseconds.
77
+ */
78
+ asNanos(): number;
79
+ /**
80
+ * Returns the total duration in microseconds.
81
+ */
82
+ asMicros(): number;
83
+ /**
84
+ * Returns the total duration in milliseconds.
85
+ */
86
+ asMillis(): number;
87
+ /**
88
+ * Returns the total duration in seconds.
89
+ */
90
+ asSecs(): number;
91
+ /**
92
+ * Returns the total duration in minutes.
93
+ */
94
+ asMinutes(): number;
95
+ /**
96
+ * Returns the total duration in hours.
97
+ */
98
+ asHours(): number;
99
+ /**
100
+ * Returns the sum of this duration and another.
101
+ *
102
+ * If the result would be negative or overflow, it saturates at zero
103
+ * or throws respectively.
104
+ */
105
+ add(other: Duration): Duration;
106
+ /**
107
+ * Returns the difference between this duration and another.
108
+ *
109
+ * If the result would be negative, it saturates at zero.
110
+ */
111
+ sub(other: Duration): Duration;
112
+ /**
113
+ * Multiplies this duration by a factor.
114
+ *
115
+ * Negative or non-finite results saturate at zero or throw.
116
+ */
117
+ mul(factor: number): Duration;
118
+ /**
119
+ * Divides this duration by a divisor.
120
+ *
121
+ * @throws {RangeError} If divisor is zero
122
+ */
123
+ div(divisor: number): Duration;
124
+ /**
125
+ * Returns true if both durations are equal.
126
+ */
127
+ eq(other: Duration): boolean;
128
+ /**
129
+ * Returns true if this duration is less than the other.
130
+ */
131
+ lt(other: Duration): boolean;
132
+ /**
133
+ * Returns true if this duration is less than or equal to the other.
134
+ */
135
+ le(other: Duration): boolean;
136
+ /**
137
+ * Returns true if this duration is greater than the other.
138
+ */
139
+ gt(other: Duration): boolean;
140
+ /**
141
+ * Returns true if this duration is greater than or equal to the other.
142
+ */
143
+ ge(other: Duration): boolean;
144
+ /**
145
+ * Returns true if this duration is zero.
146
+ */
147
+ isZero(): boolean;
148
+ /**
149
+ * Clamps this duration between a minimum and maximum.
150
+ *
151
+ * @throws {RangeError} If min is greater than max
152
+ */
153
+ clamp(min: Duration, max: Duration): Duration;
154
+ /**
155
+ * Returns a human-readable string representation.
156
+ *
157
+ * Chooses the largest unit that divides evenly.
158
+ * Values are rounded to 6 decimal places when needed.
159
+ */
160
+ toString(): string;
161
+ }
162
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;;GAcG;AACH,qBAAa,QAAQ;IACnB,kCAAkC;IAClC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAO;IAE3C,kCAAkC;IAClC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAO;IAE3C,6BAA6B;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAO;IAE5C,6BAA6B;IAC7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAA+B;IAEpE,2BAA2B;IAC3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAElE;;OAEG;IACH,gBAAuB,IAAI,WAA+B;IAE1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiB;IAE7C;;;;;OAKG;IACH,OAAO;IAQP;;;;OAIG;WACW,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ;IAIhD;;;;OAIG;WACW,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAIlD;;;;OAIG;WACW,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAIlD;;;;OAIG;WACW,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ;IAI9C;;;;OAIG;WACW,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ;IAIpD;;;;OAIG;WACW,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ;IAQhD;;OAEG;IACI,OAAO,IAAI,MAAM;IAIxB;;OAEG;IACI,QAAQ,IAAI,MAAM;IAIzB;;OAEG;IACI,QAAQ,IAAI,MAAM;IAIzB;;OAEG;IACI,MAAM,IAAI,MAAM;IAIvB;;OAEG;IACI,SAAS,IAAI,MAAM;IAI1B;;OAEG;IACI,OAAO,IAAI,MAAM;IAUxB;;;;;OAKG;IACI,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ;IAIrC;;;;OAIG;IACI,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ;IAIrC;;;;OAIG;IACI,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAIpC;;;;OAIG;IACI,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ;IAYrC;;OAEG;IACI,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO;IAInC;;OAEG;IACI,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO;IAInC;;OAEG;IACI,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO;IAInC;;OAEG;IACI,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO;IAInC;;OAEG;IACI,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO;IAQnC;;OAEG;IACI,MAAM,IAAI,OAAO;IAIxB;;;;OAIG;IACI,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,GAAG,QAAQ;IAYpD;;;;;OAKG;IACI,QAAQ,IAAI,MAAM;CAqB1B"}
package/dist/index.js ADDED
@@ -0,0 +1,267 @@
1
+ /* ===========================
2
+ * PositiveNumber (unforgeable)
3
+ * =========================== */
4
+ /**
5
+ * Converts a number into a {@link PositiveNumber} using saturating semantics.
6
+ *
7
+ * - Non-finite values (`NaN`, `Infinity`, `-Infinity`) throw.
8
+ * - Negative values are clamped to `0`.
9
+ *
10
+ * @param num - Input number
11
+ * @returns A non-negative finite number
12
+ * @throws {RangeError} If the value is not finite
13
+ */
14
+ function toPositive(num) {
15
+ if (!Number.isFinite(num)) {
16
+ throw new RangeError("Value must be finite");
17
+ }
18
+ // Saturating semantics: never negative
19
+ return (num <= 0 ? 0 : num);
20
+ }
21
+ /* ===========================
22
+ * Duration
23
+ * =========================== */
24
+ /**
25
+ * Represents an immutable, non-negative span of time.
26
+ *
27
+ * ## Design characteristics
28
+ * - Internally stored as nanoseconds
29
+ * - Saturating arithmetic (never becomes negative)
30
+ * - No calendar, timezone, or locale semantics
31
+ * - Explicit unit construction and observation
32
+ *
33
+ * This type is intended for:
34
+ * - Timeouts
35
+ * - Intervals
36
+ * - Backoff calculations
37
+ * - System and infrastructure code
38
+ */
39
+ export class Duration {
40
+ /**
41
+ * Internal constructor.
42
+ *
43
+ * All instances are guaranteed to contain a non-negative,
44
+ * finite number of nanoseconds.
45
+ */
46
+ constructor(nanoseconds) {
47
+ this.nanoseconds = nanoseconds;
48
+ }
49
+ /* ============
50
+ * Constructors
51
+ * ============ */
52
+ /**
53
+ * Creates a duration from nanoseconds.
54
+ *
55
+ * @param nanos - Number of nanoseconds
56
+ */
57
+ static fromNanos(nanos) {
58
+ return new Duration(toPositive(nanos));
59
+ }
60
+ /**
61
+ * Creates a duration from microseconds.
62
+ *
63
+ * @param micros - Number of microseconds
64
+ */
65
+ static fromMicros(micros) {
66
+ return new Duration(toPositive(micros * Duration.NS_PER_MICRO));
67
+ }
68
+ /**
69
+ * Creates a duration from milliseconds.
70
+ *
71
+ * @param millis - Number of milliseconds
72
+ */
73
+ static fromMillis(millis) {
74
+ return new Duration(toPositive(millis * Duration.NS_PER_MILLI));
75
+ }
76
+ /**
77
+ * Creates a duration from seconds.
78
+ *
79
+ * @param secs - Number of seconds
80
+ */
81
+ static fromSecs(secs) {
82
+ return new Duration(toPositive(secs * Duration.NS_PER_SECOND));
83
+ }
84
+ /**
85
+ * Creates a duration from minutes.
86
+ *
87
+ * @param minutes - Number of minutes
88
+ */
89
+ static fromMinutes(minutes) {
90
+ return new Duration(toPositive(minutes * Duration.NS_PER_MINUTE));
91
+ }
92
+ /**
93
+ * Creates a duration from hours.
94
+ *
95
+ * @param hours - Number of hours
96
+ */
97
+ static fromHours(hours) {
98
+ return new Duration(toPositive(hours * Duration.NS_PER_HOUR));
99
+ }
100
+ /* =========
101
+ * Observers
102
+ * ========= */
103
+ /**
104
+ * Returns the total duration in nanoseconds.
105
+ */
106
+ asNanos() {
107
+ return this.nanoseconds;
108
+ }
109
+ /**
110
+ * Returns the total duration in microseconds.
111
+ */
112
+ asMicros() {
113
+ return this.nanoseconds / Duration.NS_PER_MICRO;
114
+ }
115
+ /**
116
+ * Returns the total duration in milliseconds.
117
+ */
118
+ asMillis() {
119
+ return this.nanoseconds / Duration.NS_PER_MILLI;
120
+ }
121
+ /**
122
+ * Returns the total duration in seconds.
123
+ */
124
+ asSecs() {
125
+ return this.nanoseconds / Duration.NS_PER_SECOND;
126
+ }
127
+ /**
128
+ * Returns the total duration in minutes.
129
+ */
130
+ asMinutes() {
131
+ return this.nanoseconds / Duration.NS_PER_MINUTE;
132
+ }
133
+ /**
134
+ * Returns the total duration in hours.
135
+ */
136
+ asHours() {
137
+ return this.nanoseconds / Duration.NS_PER_HOUR;
138
+ }
139
+ /* ==========
140
+ * Arithmetic
141
+ * ==========
142
+ * Saturating semantics
143
+ */
144
+ /**
145
+ * Returns the sum of this duration and another.
146
+ *
147
+ * If the result would be negative or overflow, it saturates at zero
148
+ * or throws respectively.
149
+ */
150
+ add(other) {
151
+ return new Duration(toPositive(this.nanoseconds + other.nanoseconds));
152
+ }
153
+ /**
154
+ * Returns the difference between this duration and another.
155
+ *
156
+ * If the result would be negative, it saturates at zero.
157
+ */
158
+ sub(other) {
159
+ return new Duration(toPositive(this.nanoseconds - other.nanoseconds));
160
+ }
161
+ /**
162
+ * Multiplies this duration by a factor.
163
+ *
164
+ * Negative or non-finite results saturate at zero or throw.
165
+ */
166
+ mul(factor) {
167
+ return new Duration(toPositive(this.nanoseconds * factor));
168
+ }
169
+ /**
170
+ * Divides this duration by a divisor.
171
+ *
172
+ * @throws {RangeError} If divisor is zero
173
+ */
174
+ div(divisor) {
175
+ if (divisor === 0) {
176
+ throw new RangeError("Division by zero");
177
+ }
178
+ return new Duration(toPositive(this.nanoseconds / divisor));
179
+ }
180
+ /* ===========
181
+ * Comparisons
182
+ * =========== */
183
+ /**
184
+ * Returns true if both durations are equal.
185
+ */
186
+ eq(other) {
187
+ return this.nanoseconds === other.nanoseconds;
188
+ }
189
+ /**
190
+ * Returns true if this duration is less than the other.
191
+ */
192
+ lt(other) {
193
+ return this.nanoseconds < other.nanoseconds;
194
+ }
195
+ /**
196
+ * Returns true if this duration is less than or equal to the other.
197
+ */
198
+ le(other) {
199
+ return this.nanoseconds <= other.nanoseconds;
200
+ }
201
+ /**
202
+ * Returns true if this duration is greater than the other.
203
+ */
204
+ gt(other) {
205
+ return this.nanoseconds > other.nanoseconds;
206
+ }
207
+ /**
208
+ * Returns true if this duration is greater than or equal to the other.
209
+ */
210
+ ge(other) {
211
+ return this.nanoseconds >= other.nanoseconds;
212
+ }
213
+ /* =========
214
+ * Utilities
215
+ * ========= */
216
+ /**
217
+ * Returns true if this duration is zero.
218
+ */
219
+ isZero() {
220
+ return this.nanoseconds === 0;
221
+ }
222
+ /**
223
+ * Clamps this duration between a minimum and maximum.
224
+ *
225
+ * @throws {RangeError} If min is greater than max
226
+ */
227
+ clamp(min, max) {
228
+ if (min.gt(max)) {
229
+ throw new RangeError("min must be <= max");
230
+ }
231
+ return new Duration(toPositive(Math.min(Math.max(this.nanoseconds, min.nanoseconds), max.nanoseconds)));
232
+ }
233
+ /**
234
+ * Returns a human-readable string representation.
235
+ *
236
+ * Chooses the largest unit that divides evenly.
237
+ * Values are rounded to 6 decimal places when needed.
238
+ */
239
+ toString() {
240
+ const round = (n) => Number.isInteger(n) ? n : Number(n.toFixed(6));
241
+ if (this.nanoseconds % Duration.NS_PER_HOUR === 0)
242
+ return `${round(this.asHours())}h`;
243
+ if (this.nanoseconds % Duration.NS_PER_MINUTE === 0)
244
+ return `${round(this.asMinutes())}m`;
245
+ if (this.nanoseconds % Duration.NS_PER_SECOND === 0)
246
+ return `${round(this.asSecs())}s`;
247
+ if (this.nanoseconds % Duration.NS_PER_MILLI === 0)
248
+ return `${round(this.asMillis())}ms`;
249
+ if (this.nanoseconds % Duration.NS_PER_MICRO === 0)
250
+ return `${round(this.asMicros())}µs`;
251
+ return `${this.nanoseconds}ns`;
252
+ }
253
+ }
254
+ /** Nanoseconds per microsecond */
255
+ Duration.NS_PER_MICRO = 1e3;
256
+ /** Nanoseconds per millisecond */
257
+ Duration.NS_PER_MILLI = 1e6;
258
+ /** Nanoseconds per second */
259
+ Duration.NS_PER_SECOND = 1e9;
260
+ /** Nanoseconds per minute */
261
+ Duration.NS_PER_MINUTE = 60 * Duration.NS_PER_SECOND;
262
+ /** Nanoseconds per hour */
263
+ Duration.NS_PER_HOUR = 60 * Duration.NS_PER_MINUTE;
264
+ /**
265
+ * A duration of zero length.
266
+ */
267
+ Duration.ZERO = new Duration(toPositive(0));
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "nanoduration",
3
+ "version": "1.0.0",
4
+ "description": "An immutable, non-negative, Rust-inspired `Duration` type for JavaScript and TypeScript",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "duration",
23
+ "time",
24
+ "typescript",
25
+ "immutable",
26
+ "esm",
27
+ "rust",
28
+ "value-object"
29
+ ],
30
+ "license": "MIT"
31
+ }