@mantiq/helpers 0.0.1

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.
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Fluent duration builder for human-readable time math.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * Duration.minutes(5).plus(Duration.seconds(30)).toMs() // 330_000
7
+ * Duration.hours(2).toHuman() // '2 hours'
8
+ * Duration.ms(90_000).toHuman() // '1 minute, 30 seconds'
9
+ * Duration.parse('2h30m').toSeconds() // 9000
10
+ * ```
11
+ */
12
+ export class Duration {
13
+ private constructor(private readonly milliseconds: number) {}
14
+
15
+ // ── Factories ───────────────────────────────────────────────────
16
+
17
+ static ms(value: number): Duration { return new Duration(value) }
18
+ static seconds(value: number): Duration { return new Duration(value * 1000) }
19
+ static minutes(value: number): Duration { return new Duration(value * 60_000) }
20
+ static hours(value: number): Duration { return new Duration(value * 3_600_000) }
21
+ static days(value: number): Duration { return new Duration(value * 86_400_000) }
22
+ static weeks(value: number): Duration { return new Duration(value * 604_800_000) }
23
+
24
+ /** Parse a duration string like '2h30m', '1d12h', '500ms', '2.5s' */
25
+ static parse(input: string): Duration {
26
+ let totalMs = 0
27
+ const regex = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)/gi
28
+ let match: RegExpExecArray | null
29
+ while ((match = regex.exec(input)) !== null) {
30
+ const value = parseFloat(match[1]!)
31
+ const unit = match[2]!.toLowerCase()
32
+ switch (unit) {
33
+ case 'ms': totalMs += value; break
34
+ case 's': totalMs += value * 1000; break
35
+ case 'm': totalMs += value * 60_000; break
36
+ case 'h': totalMs += value * 3_600_000; break
37
+ case 'd': totalMs += value * 86_400_000; break
38
+ case 'w': totalMs += value * 604_800_000; break
39
+ }
40
+ }
41
+ if (totalMs === 0 && /^\d+$/.test(input.trim())) {
42
+ totalMs = parseInt(input.trim(), 10)
43
+ }
44
+ return new Duration(totalMs)
45
+ }
46
+
47
+ /** Create from a start and end date */
48
+ static between(start: Date, end: Date): Duration {
49
+ return new Duration(Math.abs(end.getTime() - start.getTime()))
50
+ }
51
+
52
+ // ── Arithmetic ──────────────────────────────────────────────────
53
+
54
+ plus(other: Duration): Duration {
55
+ return new Duration(this.milliseconds + other.milliseconds)
56
+ }
57
+
58
+ minus(other: Duration): Duration {
59
+ return new Duration(Math.max(0, this.milliseconds - other.milliseconds))
60
+ }
61
+
62
+ times(factor: number): Duration {
63
+ return new Duration(this.milliseconds * factor)
64
+ }
65
+
66
+ dividedBy(divisor: number): Duration {
67
+ return new Duration(Math.floor(this.milliseconds / divisor))
68
+ }
69
+
70
+ // ── Conversions ─────────────────────────────────────────────────
71
+
72
+ toMs(): number { return this.milliseconds }
73
+ toSeconds(): number { return this.milliseconds / 1000 }
74
+ toMinutes(): number { return this.milliseconds / 60_000 }
75
+ toHours(): number { return this.milliseconds / 3_600_000 }
76
+ toDays(): number { return this.milliseconds / 86_400_000 }
77
+ toWeeks(): number { return this.milliseconds / 604_800_000 }
78
+
79
+ /** Get an object with each unit broken down */
80
+ toComponents(): {
81
+ days: number
82
+ hours: number
83
+ minutes: number
84
+ seconds: number
85
+ milliseconds: number
86
+ } {
87
+ let remaining = this.milliseconds
88
+ const days = Math.floor(remaining / 86_400_000)
89
+ remaining -= days * 86_400_000
90
+ const hours = Math.floor(remaining / 3_600_000)
91
+ remaining -= hours * 3_600_000
92
+ const minutes = Math.floor(remaining / 60_000)
93
+ remaining -= minutes * 60_000
94
+ const seconds = Math.floor(remaining / 1000)
95
+ const milliseconds = remaining - seconds * 1000
96
+
97
+ return { days, hours, minutes, seconds, milliseconds }
98
+ }
99
+
100
+ /** Human-readable string like '2 hours, 30 minutes' */
101
+ toHuman(): string {
102
+ const { days, hours, minutes, seconds, milliseconds } = this.toComponents()
103
+ const parts: string[] = []
104
+
105
+ if (days > 0) parts.push(`${days} ${days === 1 ? 'day' : 'days'}`)
106
+ if (hours > 0) parts.push(`${hours} ${hours === 1 ? 'hour' : 'hours'}`)
107
+ if (minutes > 0) parts.push(`${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`)
108
+ if (seconds > 0) parts.push(`${seconds} ${seconds === 1 ? 'second' : 'seconds'}`)
109
+ if (milliseconds > 0 && parts.length === 0) parts.push(`${milliseconds}ms`)
110
+
111
+ return parts.length > 0 ? parts.join(', ') : '0ms'
112
+ }
113
+
114
+ /** Compact string like '2h30m' */
115
+ toCompact(): string {
116
+ const { days, hours, minutes, seconds, milliseconds } = this.toComponents()
117
+ const parts: string[] = []
118
+
119
+ if (days > 0) parts.push(`${days}d`)
120
+ if (hours > 0) parts.push(`${hours}h`)
121
+ if (minutes > 0) parts.push(`${minutes}m`)
122
+ if (seconds > 0) parts.push(`${seconds}s`)
123
+ if (milliseconds > 0 && parts.length === 0) parts.push(`${milliseconds}ms`)
124
+
125
+ return parts.join('') || '0ms'
126
+ }
127
+
128
+ /** ISO 8601 duration string (e.g. PT2H30M) */
129
+ toISO(): string {
130
+ const { days, hours, minutes, seconds } = this.toComponents()
131
+ let iso = 'P'
132
+ if (days > 0) iso += `${days}D`
133
+ if (hours > 0 || minutes > 0 || seconds > 0) {
134
+ iso += 'T'
135
+ if (hours > 0) iso += `${hours}H`
136
+ if (minutes > 0) iso += `${minutes}M`
137
+ if (seconds > 0) iso += `${seconds}S`
138
+ }
139
+ return iso === 'P' ? 'PT0S' : iso
140
+ }
141
+
142
+ // ── Comparisons ─────────────────────────────────────────────────
143
+
144
+ isZero(): boolean { return this.milliseconds === 0 }
145
+ isPositive(): boolean { return this.milliseconds > 0 }
146
+
147
+ greaterThan(other: Duration): boolean { return this.milliseconds > other.milliseconds }
148
+ lessThan(other: Duration): boolean { return this.milliseconds < other.milliseconds }
149
+ equals(other: Duration): boolean { return this.milliseconds === other.milliseconds }
150
+
151
+ // ── Date arithmetic ─────────────────────────────────────────────
152
+
153
+ /** Add this duration to a date */
154
+ addTo(date: Date): Date {
155
+ return new Date(date.getTime() + this.milliseconds)
156
+ }
157
+
158
+ /** Subtract this duration from a date */
159
+ subtractFrom(date: Date): Date {
160
+ return new Date(date.getTime() - this.milliseconds)
161
+ }
162
+
163
+ /** Get a date that is this duration from now */
164
+ fromNow(): Date {
165
+ return this.addTo(new Date())
166
+ }
167
+
168
+ /** Get a date that was this duration ago */
169
+ ago(): Date {
170
+ return this.subtractFrom(new Date())
171
+ }
172
+ }