@verifyhash/iso-duration 0.1.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 +153 -0
- package/index.js +293 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 <OWNER-NAME PLACEHOLDER>
|
|
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
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# iso-duration
|
|
2
|
+
|
|
3
|
+
<!-- publish-prep -->
|
|
4
|
+
> **TODO (owner): pick the final npm name/scope before publishing.**
|
|
5
|
+
> The npm package name is **not** finalized by this project. Convention used here:
|
|
6
|
+
> `package.json` keeps the working slug `iso-duration` as a placeholder — replace it
|
|
7
|
+
> (and the `npm install` line below) with the real published name/scope before
|
|
8
|
+
> running `npm publish`.
|
|
9
|
+
|
|
10
|
+
A tiny, **zero-dependency, zero-network** Node.js library for **ISO 8601
|
|
11
|
+
durations** — the `P3Y6M4DT12H30M5S` strings that show up in JSON Schema
|
|
12
|
+
(`format: duration`), OpenAPI, iCalendar (`RRULE`/`DURATION`), GitHub Actions
|
|
13
|
+
timeouts, Kubernetes manifests, and countless "retry after / cache for / repeat
|
|
14
|
+
every" API fields.
|
|
15
|
+
|
|
16
|
+
It does four things and nothing else:
|
|
17
|
+
|
|
18
|
+
- **`parse(str)`** — string → duration object
|
|
19
|
+
- **`format(obj)`** — duration object → canonical string (round-trips `parse`)
|
|
20
|
+
- **`addToDate` / `subtractFromDate`** — calendar-correct `Date` arithmetic
|
|
21
|
+
- **`humanize(obj)`** — `"3 hours 30 minutes"`
|
|
22
|
+
|
|
23
|
+
No dependencies, no install step, no I/O. Just `index.js` (CommonJS) and pure
|
|
24
|
+
functions. Drop it into any project.
|
|
25
|
+
|
|
26
|
+
## Who it's for
|
|
27
|
+
|
|
28
|
+
JavaScript/Node developers who receive or emit ISO 8601 durations from APIs,
|
|
29
|
+
schemas, or schedules and want to parse, normalize, do date math on, or display
|
|
30
|
+
them — without pulling in Luxon/moment/date-fns just for the duration bits.
|
|
31
|
+
|
|
32
|
+
## The duration object
|
|
33
|
+
|
|
34
|
+
Every function speaks the same plain object. All keys are optional; missing
|
|
35
|
+
numeric keys are `0`, and `negative` defaults to `false`:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
{ years, months, weeks, days, hours, minutes, seconds, negative }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
const iso = require('iso-duration'); // or require('./index.js')
|
|
45
|
+
|
|
46
|
+
iso.parse('P3Y6M4DT12H30M5S');
|
|
47
|
+
// { years: 3, months: 6, weeks: 0, days: 4,
|
|
48
|
+
// hours: 12, minutes: 30, seconds: 5, negative: false }
|
|
49
|
+
|
|
50
|
+
iso.parse('-PT1H30M'); // { ..., hours: 1, minutes: 30, negative: true }
|
|
51
|
+
iso.parse('P2W'); // { ..., weeks: 2 }
|
|
52
|
+
iso.parse('PT0.5H'); // { ..., hours: 0.5 } (fractional final component)
|
|
53
|
+
|
|
54
|
+
iso.format({ hours: 3, minutes: 30 }); // 'PT3H30M'
|
|
55
|
+
iso.format({}); // 'PT0S'
|
|
56
|
+
iso.format({ weeks: 2 }); // 'P2W'
|
|
57
|
+
iso.format({ hours: 1, negative: true }); // '-PT1H'
|
|
58
|
+
|
|
59
|
+
iso.humanize({ hours: 3, minutes: 30 }); // '3 hours 30 minutes'
|
|
60
|
+
iso.humanize({ hours: 1 }); // '1 hour'
|
|
61
|
+
iso.humanize({}); // '0 seconds'
|
|
62
|
+
|
|
63
|
+
const start = new Date('2020-01-31T00:00:00Z');
|
|
64
|
+
iso.addToDate(start, { months: 1 }); // 2020-02-29T00:00:00.000Z
|
|
65
|
+
start.toISOString(); // unchanged — input is never mutated
|
|
66
|
+
iso.subtractFromDate(start, { days: 10 }); // 2020-01-21T00:00:00.000Z
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Two design choices to know about
|
|
70
|
+
|
|
71
|
+
Libraries genuinely disagree on these two points, so they are spelled out here
|
|
72
|
+
rather than left to surprise you.
|
|
73
|
+
|
|
74
|
+
### 1. Weeks normalization
|
|
75
|
+
|
|
76
|
+
ISO 8601 treats `W` (weeks) as mutually exclusive with the other date fields —
|
|
77
|
+
`P2W` is legal, `P1W3D` technically is not. This library is lenient on **input**
|
|
78
|
+
(it will parse `P1W3D` and keep `weeks` and `days` separate) but opinionated on
|
|
79
|
+
**output**:
|
|
80
|
+
|
|
81
|
+
- `format()` emits `P2W` **only when weeks is the sole non-zero component.**
|
|
82
|
+
- In every other case weeks are folded into days at **1 week = 7 days**
|
|
83
|
+
(e.g. `format({ weeks: 1, days: 1 })` → `'P8D'`).
|
|
84
|
+
|
|
85
|
+
This keeps the canonical string valid ISO and guarantees `format` round-trips
|
|
86
|
+
through `parse`: `format(parse(s)) === s` for any canonical `s`, and repeated
|
|
87
|
+
`format(parse(...))` is stable. In date arithmetic, weeks are always treated as
|
|
88
|
+
7 elapsed days.
|
|
89
|
+
|
|
90
|
+
### 2. Month (and year) overflow clamps
|
|
91
|
+
|
|
92
|
+
Adding months and years is calendar arithmetic, and calendars have ragged month
|
|
93
|
+
lengths. When the target month is shorter than the source day-of-month, this
|
|
94
|
+
library **clamps to the last day of the target month** — the same rule Luxon,
|
|
95
|
+
Temporal, and date-fns use:
|
|
96
|
+
|
|
97
|
+
- `Jan 31 + P1M` → **Feb 29** in a leap year, **Feb 28** otherwise.
|
|
98
|
+
- `Mar 31 − P1M` → **Feb 29 / Feb 28** (subtraction clamps the same way).
|
|
99
|
+
|
|
100
|
+
The arithmetic order is: **years/months are applied first as whole calendar
|
|
101
|
+
steps (with clamping), then weeks/days/hours/minutes/seconds are added as a flat
|
|
102
|
+
elapsed-milliseconds offset.** Because the calendar step happens first, results
|
|
103
|
+
are deterministic and independent of your machine's timezone (all math is done
|
|
104
|
+
in UTC) and the input `Date` is never mutated — a new `Date` is returned.
|
|
105
|
+
|
|
106
|
+
**Honest limits.** Because day/time components are applied as elapsed
|
|
107
|
+
milliseconds, they do **not** track daylight-saving wall-clock shifts — that is
|
|
108
|
+
correct for UTC/absolute instants but means "+1 day" is always exactly 24 hours,
|
|
109
|
+
not "same local time tomorrow." Fractional **years or months** (rare, e.g.
|
|
110
|
+
`P0.5M`) can't be expressed as an exact number of calendar days, so their
|
|
111
|
+
fractional part is approximated using the mean Gregorian month (30.436875 days);
|
|
112
|
+
integer years/months — the normal case — are always exact.
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
`parse()` throws a descriptive `Error` on malformed input, including: an empty
|
|
117
|
+
string, a missing `P`, a `T` separator with no time components (`"PT"`), a bare
|
|
118
|
+
`"P"`, out-of-order fields (`"P3M6Y"`), and time letters outside the `T` block
|
|
119
|
+
(`"P5S"`).
|
|
120
|
+
|
|
121
|
+
## Running the tests
|
|
122
|
+
|
|
123
|
+
One command, no install, no network:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
node test/index.test.js
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
It prints one line per test and exits non-zero if any fail. Coverage includes
|
|
130
|
+
parse/format round-trips, `P2W` weeks, `PT0S` and negatives, fractional seconds,
|
|
131
|
+
Jan-31 + 1-month month-overflow arithmetic, the no-mutation guarantee, humanize
|
|
132
|
+
pluralization, and malformed-input throw cases.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT.
|
|
137
|
+
|
|
138
|
+
## Install
|
|
139
|
+
|
|
140
|
+
> Placeholder name: the `iso-duration` below is the working slug, not a finalized
|
|
141
|
+
> npm name — see the owner TODO near the top of this README.
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
npm install iso-duration
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const iso = require('iso-duration');
|
|
149
|
+
|
|
150
|
+
iso.parse('P3Y6M4DT12H30M5S'); // { years:3, months:6, days:4, hours:12, minutes:30, seconds:5, ... }
|
|
151
|
+
iso.format({ hours: 3, minutes: 30 }); // 'PT3H30M'
|
|
152
|
+
iso.humanize({ hours: 1, minutes: 30 }); // '1 hour 30 minutes'
|
|
153
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* iso-duration — zero-dependency ISO 8601 duration parse / format /
|
|
5
|
+
* arithmetic / humanize for Node.js.
|
|
6
|
+
*
|
|
7
|
+
* A "duration object" is a plain object with any of these numeric keys plus a
|
|
8
|
+
* boolean `negative` flag:
|
|
9
|
+
*
|
|
10
|
+
* { years, months, weeks, days, hours, minutes, seconds, negative }
|
|
11
|
+
*
|
|
12
|
+
* All keys are optional; missing numeric keys are treated as 0 and `negative`
|
|
13
|
+
* defaults to false. Every function here is pure — no I/O, no network, no
|
|
14
|
+
* mutation of its arguments.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var UNIT_KEYS = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'];
|
|
18
|
+
|
|
19
|
+
// Average calendar month / year, used ONLY for the rare case of a *fractional*
|
|
20
|
+
// year or month in date arithmetic (see addToDate). 30.436875 days is the mean
|
|
21
|
+
// Gregorian month (365.2425 / 12).
|
|
22
|
+
var MS_PER_DAY = 86400000;
|
|
23
|
+
var AVG_MONTH_MS = 30.436875 * MS_PER_DAY;
|
|
24
|
+
var AVG_YEAR_MS = 365.2425 * MS_PER_DAY;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// parse
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
// P[n]Y[n]M[n]W[n]D [ T[n]H[n]M[n]S ]. A leading '-' (or '+') sets the sign.
|
|
31
|
+
// Numbers may carry a fractional part with '.' or ',' as the decimal mark.
|
|
32
|
+
var NUM = '(\\d+(?:[.,]\\d+)?)';
|
|
33
|
+
var DUR_RE = new RegExp(
|
|
34
|
+
'^([+-])?P' +
|
|
35
|
+
'(?:' + NUM + 'Y)?' +
|
|
36
|
+
'(?:' + NUM + 'M)?' +
|
|
37
|
+
'(?:' + NUM + 'W)?' +
|
|
38
|
+
'(?:' + NUM + 'D)?' +
|
|
39
|
+
'(T' +
|
|
40
|
+
'(?:' + NUM + 'H)?' +
|
|
41
|
+
'(?:' + NUM + 'M)?' +
|
|
42
|
+
'(?:' + NUM + 'S)?' +
|
|
43
|
+
')?$'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
function toNumber(raw) {
|
|
47
|
+
if (raw === undefined) return 0;
|
|
48
|
+
return parseFloat(raw.replace(',', '.'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse an ISO 8601 duration string into a duration object.
|
|
53
|
+
* Throws an Error with a clear message on malformed input.
|
|
54
|
+
*/
|
|
55
|
+
function parse(str) {
|
|
56
|
+
if (typeof str !== 'string') {
|
|
57
|
+
throw new Error('iso-duration.parse: expected a string, got ' + typeof str);
|
|
58
|
+
}
|
|
59
|
+
var s = str.trim();
|
|
60
|
+
if (s === '') {
|
|
61
|
+
throw new Error('iso-duration.parse: empty string is not a valid duration');
|
|
62
|
+
}
|
|
63
|
+
var m = DUR_RE.exec(s);
|
|
64
|
+
if (!m) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'iso-duration.parse: "' + str + '" is not a valid ISO 8601 duration'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var sign = m[1];
|
|
71
|
+
var years = m[2];
|
|
72
|
+
var months = m[3];
|
|
73
|
+
var weeks = m[4];
|
|
74
|
+
var days = m[5];
|
|
75
|
+
var timeBlock = m[6]; // the literal 'T...' block, or undefined
|
|
76
|
+
var hours = m[7];
|
|
77
|
+
var minutes = m[8];
|
|
78
|
+
var seconds = m[9];
|
|
79
|
+
|
|
80
|
+
var hasDate =
|
|
81
|
+
years !== undefined ||
|
|
82
|
+
months !== undefined ||
|
|
83
|
+
weeks !== undefined ||
|
|
84
|
+
days !== undefined;
|
|
85
|
+
var hasTime =
|
|
86
|
+
hours !== undefined || minutes !== undefined || seconds !== undefined;
|
|
87
|
+
|
|
88
|
+
// A 'T' designator was present but no time components followed it.
|
|
89
|
+
if (timeBlock !== undefined && !hasTime) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'iso-duration.parse: "' + str + '" has a "T" separator but no time components'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
// "P" (or "-P") alone — no components at all.
|
|
95
|
+
if (!hasDate && !hasTime) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'iso-duration.parse: "' + str + '" has no duration components'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
years: toNumber(years),
|
|
103
|
+
months: toNumber(months),
|
|
104
|
+
weeks: toNumber(weeks),
|
|
105
|
+
days: toNumber(days),
|
|
106
|
+
hours: toNumber(hours),
|
|
107
|
+
minutes: toNumber(minutes),
|
|
108
|
+
seconds: toNumber(seconds),
|
|
109
|
+
negative: sign === '-'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// format
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
function num(obj, key) {
|
|
118
|
+
var v = obj[key];
|
|
119
|
+
return typeof v === 'number' && !isNaN(v) ? v : 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Render a number without a trailing ".0" and without locale grouping.
|
|
123
|
+
function fmtNum(n) {
|
|
124
|
+
return String(n);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format a duration object into a canonical ISO 8601 string.
|
|
129
|
+
*
|
|
130
|
+
* Rules:
|
|
131
|
+
* - Zero components are omitted.
|
|
132
|
+
* - An all-zero duration formats as "PT0S".
|
|
133
|
+
* - A negative duration gets a leading "-".
|
|
134
|
+
* - Weeks are emitted as "P2W" ONLY when weeks is the sole non-zero
|
|
135
|
+
* component; otherwise weeks are folded into days (1 week = 7 days) so the
|
|
136
|
+
* output round-trips through parse().
|
|
137
|
+
*/
|
|
138
|
+
function format(obj) {
|
|
139
|
+
if (obj == null || typeof obj !== 'object') {
|
|
140
|
+
throw new Error('iso-duration.format: expected a duration object');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var years = num(obj, 'years');
|
|
144
|
+
var months = num(obj, 'months');
|
|
145
|
+
var weeks = num(obj, 'weeks');
|
|
146
|
+
var days = num(obj, 'days');
|
|
147
|
+
var hours = num(obj, 'hours');
|
|
148
|
+
var minutes = num(obj, 'minutes');
|
|
149
|
+
var seconds = num(obj, 'seconds');
|
|
150
|
+
|
|
151
|
+
var weeksIsSole =
|
|
152
|
+
weeks !== 0 &&
|
|
153
|
+
years === 0 &&
|
|
154
|
+
months === 0 &&
|
|
155
|
+
days === 0 &&
|
|
156
|
+
hours === 0 &&
|
|
157
|
+
minutes === 0 &&
|
|
158
|
+
seconds === 0;
|
|
159
|
+
|
|
160
|
+
var out = obj.negative ? '-P' : 'P';
|
|
161
|
+
|
|
162
|
+
if (weeksIsSole) {
|
|
163
|
+
return out + fmtNum(weeks) + 'W';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fold weeks into days for every other case.
|
|
167
|
+
days = days + weeks * 7;
|
|
168
|
+
|
|
169
|
+
if (years !== 0) out += fmtNum(years) + 'Y';
|
|
170
|
+
if (months !== 0) out += fmtNum(months) + 'M';
|
|
171
|
+
if (days !== 0) out += fmtNum(days) + 'D';
|
|
172
|
+
|
|
173
|
+
if (hours !== 0 || minutes !== 0 || seconds !== 0) {
|
|
174
|
+
out += 'T';
|
|
175
|
+
if (hours !== 0) out += fmtNum(hours) + 'H';
|
|
176
|
+
if (minutes !== 0) out += fmtNum(minutes) + 'M';
|
|
177
|
+
if (seconds !== 0) out += fmtNum(seconds) + 'S';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// All-zero duration.
|
|
181
|
+
if (out === 'P' || out === '-P') {
|
|
182
|
+
return 'PT0S';
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// date arithmetic
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function daysInMonth(year, monthIndex) {
|
|
192
|
+
// monthIndex 0-11; day 0 of next month = last day of this month.
|
|
193
|
+
return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Add an integer number of calendar months (may be negative), clamping the
|
|
197
|
+
// day-of-month on overflow (Jan 31 + 1 month -> Feb 28/29).
|
|
198
|
+
function addWholeMonths(d, months) {
|
|
199
|
+
if (months === 0) return;
|
|
200
|
+
var day = d.getUTCDate();
|
|
201
|
+
d.setUTCDate(1);
|
|
202
|
+
d.setUTCMonth(d.getUTCMonth() + months);
|
|
203
|
+
var dim = daysInMonth(d.getUTCFullYear(), d.getUTCMonth());
|
|
204
|
+
d.setUTCDate(Math.min(day, dim));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// direction: +1 for addToDate, -1 for subtractFromDate.
|
|
208
|
+
function applyDuration(date, obj, direction) {
|
|
209
|
+
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
|
210
|
+
throw new Error('iso-duration: first argument must be a valid Date');
|
|
211
|
+
}
|
|
212
|
+
if (obj == null || typeof obj !== 'object') {
|
|
213
|
+
throw new Error('iso-duration: second argument must be a duration object');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
var sign = (obj.negative ? -1 : 1) * direction;
|
|
217
|
+
|
|
218
|
+
var years = num(obj, 'years');
|
|
219
|
+
var months = num(obj, 'months');
|
|
220
|
+
|
|
221
|
+
var totalMonths = sign * (years * 12 + months);
|
|
222
|
+
var wholeMonths = totalMonths < 0 ? Math.ceil(totalMonths) : Math.floor(totalMonths);
|
|
223
|
+
var fracMonths = totalMonths - wholeMonths;
|
|
224
|
+
|
|
225
|
+
// 1) Calendar-correct year/month step first (with day clamping).
|
|
226
|
+
var result = new Date(date.getTime());
|
|
227
|
+
addWholeMonths(result, wholeMonths);
|
|
228
|
+
|
|
229
|
+
// 2) Everything else as elapsed milliseconds. Any fractional month/year
|
|
230
|
+
// remainder is approximated with the mean Gregorian month length.
|
|
231
|
+
var elapsedMs =
|
|
232
|
+
sign *
|
|
233
|
+
(num(obj, 'weeks') * 7 * MS_PER_DAY +
|
|
234
|
+
num(obj, 'days') * MS_PER_DAY +
|
|
235
|
+
num(obj, 'hours') * 3600000 +
|
|
236
|
+
num(obj, 'minutes') * 60000 +
|
|
237
|
+
num(obj, 'seconds') * 1000) +
|
|
238
|
+
fracMonths * AVG_MONTH_MS;
|
|
239
|
+
|
|
240
|
+
return new Date(result.getTime() + elapsedMs);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function addToDate(date, obj) {
|
|
244
|
+
return applyDuration(date, obj, 1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function subtractFromDate(date, obj) {
|
|
248
|
+
return applyDuration(date, obj, -1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// humanize
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
var UNIT_LABELS = {
|
|
256
|
+
years: 'year',
|
|
257
|
+
months: 'month',
|
|
258
|
+
weeks: 'week',
|
|
259
|
+
days: 'day',
|
|
260
|
+
hours: 'hour',
|
|
261
|
+
minutes: 'minute',
|
|
262
|
+
seconds: 'second'
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Render a duration object as an English phrase, e.g. "3 hours 30 minutes".
|
|
267
|
+
* Zero units are omitted; an all-zero duration returns "0 seconds".
|
|
268
|
+
* The sign is not rendered — inspect obj.negative for direction.
|
|
269
|
+
*/
|
|
270
|
+
function humanize(obj) {
|
|
271
|
+
if (obj == null || typeof obj !== 'object') {
|
|
272
|
+
throw new Error('iso-duration.humanize: expected a duration object');
|
|
273
|
+
}
|
|
274
|
+
var parts = [];
|
|
275
|
+
for (var i = 0; i < UNIT_KEYS.length; i++) {
|
|
276
|
+
var key = UNIT_KEYS[i];
|
|
277
|
+
var v = num(obj, key);
|
|
278
|
+
if (v === 0) continue;
|
|
279
|
+
var label = UNIT_LABELS[key];
|
|
280
|
+
var mag = Math.abs(v);
|
|
281
|
+
parts.push(mag + ' ' + label + (mag === 1 ? '' : 's'));
|
|
282
|
+
}
|
|
283
|
+
if (parts.length === 0) return '0 seconds';
|
|
284
|
+
return parts.join(' ');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
parse: parse,
|
|
289
|
+
format: format,
|
|
290
|
+
addToDate: addToDate,
|
|
291
|
+
subtractFromDate: subtractFromDate,
|
|
292
|
+
humanize: humanize
|
|
293
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verifyhash/iso-duration",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency ISO 8601 duration parse, format, calendar-correct date arithmetic, and English humanize for Node.js.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"iso8601",
|
|
7
|
+
"iso-8601",
|
|
8
|
+
"duration",
|
|
9
|
+
"parse",
|
|
10
|
+
"format",
|
|
11
|
+
"period",
|
|
12
|
+
"pt1h30m",
|
|
13
|
+
"humanize",
|
|
14
|
+
"date-arithmetic"
|
|
15
|
+
],
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node test/index.test.js"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/verifyhash/libs.git",
|
|
31
|
+
"directory": "iso-duration"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/verifyhash/libs/tree/main/iso-duration#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/verifyhash/libs/issues"
|
|
36
|
+
}
|
|
37
|
+
}
|