@0dep/piso
Version:
ISO 8601 interval, date, and duration parser
498 lines (358 loc) • 13.5 kB
Markdown
[](https://github.com/zerodep/piso/actions/workflows/build.yaml)[](https://coveralls.io/github/zerodep/piso?branch=main)
ISO 8601 date, duration, and interval parsing package as declared on [Wikipedia ISO 8601](https://en.wikipedia.org/wiki/ISO_8601).
> In Spain, piso refers to the whole apartment, whereas in Mexico, it refers only to the floor of your departamento.
> But the above has nothing to do with this project.
- [Api](
- [`parseInterval(iso8601Interval[, enforceUTC])`](
- [`parseDuration(iso8601Duration)`](
- [`getDate(iso8601Date[, enforceUTC])`](
- [`getISOWeekString([date])`](
- [`getUTCWeekNumber([date])`](
- [`getUTCLastWeekOfYear(Y)`](
- [`getUTCWeekOneDate(Y)`](
Parse interval from an ISO 8601 interval string.
- `iso8601Interval`: string with ISO 8601 interval source
- `enforceUTC`: optional boolean, enforce UTC if source lacks time zone offset
Returns [ISOInterval](
```javascript
import { parseInterval, ISOInterval } from '@0dep/piso';
const viableIntervals = [
'2007-03-01/2007-04-01',
'P2Y/2007-03-01T13:00:00Z',
'2007-03-01T13:00:00Z/P2Y',
'R5/P1Y/2025-05-01T13:00:00Z',
'R-1/2009-07-01T00:00Z/P1M',
'R-1/1972-07-01T00:02Z/PT1H3M',
'R-1/P1M/2024-07-27T00:00Z',
'2007-318/2007-319',
'2007-318/319T24:00:00Z',
];
for (const i of viableIntervals) {
console.log({ [i]: parseInterval(i).getExpireAt(), utc: parseInterval(i, true).getExpireAt() });
}
```
Parse duration from an ISO 8601 duration string.
- `iso8601Duration`: string with ISO 8601 duration source
Returns [ISODuration](
```javascript
import { parseDuration } from '@0dep/piso';
const viableDurations = [
'PT1M5S',
'PT1M0.5S',
'PT0.5S',
'PT0.01S',
'PT0.001S',
'PT0.0001S',
'PT0.5M',
'PT0.5H',
'PT1.5H',
'P0.5D',
'P1W',
'P0.5W',
'P0.5M',
'P0.5D',
'P1Y',
'P1Y2M3W4DT5H6M7S',
'PT0S',
'P0D',
];
for (const d of viableDurations) {
console.log({ [d]: parseDuration(d).getExpireAt() });
}
try {
// fractions are only allowed on the smallest unit
parseDuration('P0.5YT3S');
} catch (err) {
console.log({ err });
}
```
Get Date from an ISO 8601 date time string.
- `iso8601Date`: string with ISO 8601 date source, date and number are also accepted
- `enforceUTC`: optional boolean, enforce UTC if source lacks time zone offset
Returns date.
```javascript
import { getDate } from '@0dep/piso';
const viableDates = [
'2024-01-27',
'2024-02-28',
'2024-02-29',
'2020-02-29',
'2016-02-29',
'2024-W03-2',
'2024-01',
'2024-12',
'20240127',
'2024-012',
'2024012',
'2024-012T08:06:30',
'2024-02-27T08:06:30',
'2024-02-27T08:06:30.001',
'2024-02-27T08:06:30.0011',
'2024-02-27T08:06:30.0',
'2024-02-27T08:06:30,001',
'2024-02-27T08:06:30Z',
'2024-02-03T08:06:30+02:00',
'2024-02-03T08:06:30.5+02:00',
'20240203T080630+0200',
'2024-02-03T08:06:30-02:30',
'2024-02-03T08:06:30-02',
'2025-01-01T12:00:42.01-02:00',
'2025-01-01T12:00:42.01+02:30',
'2025-01-01T12:00:42.01+02:30:30',
'2025-01-01T23:59',
'2025-01-01T24:00',
'2025-01-01T24:00:00',
'2025-01-01T24:00:00.000',
'2025-01-01T24:00Z',
'2025-01-01T24:00+01',
'2025-01-01T24:00:00+01',
'2025-01-01T24:00:00.00+01',
'20240127T1200',
'20240127T120001',
'20240127T120001,001',
new Date(2024, 3, 22),
0,
Date.UTC(2024, 3, 22),
];
for (const d of viableDates) {
console.log({ [d]: getDate(d), utc: getDate(d, true) });
}
try {
getDate('2023-02-29');
} catch (err) {
console.log({ err });
}
try {
// not this year
getDate('2023-W53-1T12:00');
} catch (err) {
console.log({ err });
}
try {
// unbalanced separators
getDate('2023-02-28T1200');
} catch (err) {
console.log({ err });
}
```
> NB! string without timezone precision is considered local date, or as Wikipedia put it "If no UTC relation information is given with a time representation, the time is assumed to be in local time". Unless, of course, enforce UTC instruction is used.
Get last week of year
- `Y`: full year
Returns 52 or 53.
```javascript
import { getUTCLastWeekOfYear } from '@0dep/piso';
console.log('last week number', getUTCLastWeekOfYear(2024));
```
Get Monday week one date
- `Y`: full year
Returns date Monday week one
```javascript
import { getUTCWeekOneDate } from '@0dep/piso';
console.log('Monday week one', getUTCWeekOneDate(2021));
```
Get ISO week date string from date.
- `date`: optional date, defaults to now
```javascript
import { getISOWeekString } from '@0dep/piso';
console.log('date as week', getISOWeekString(new Date(2021, 11, 28)));
```
Get weeknumber from date.
- `date`: optional date, defaults to now
Returns:
- `Y`: full year representation of week date
- `W`: week number
- `weekday`:
```javascript
import { getUTCWeekNumber } from '@0dep/piso';
console.log(getUTCWeekNumber(new Date(2016, 0, 1)));
```
Interval instance.
**Constructor:**
- `source`: ISO8601 interval source
- `enforceUTC`: optional boolean, enforce UTC if source lacks time zone offset
**Properties:**
- `repeat`: number of repeats
- `start`: start date as [ISODate](
- `duration`: duration as [ISODuration](
- `end`: end date as [ISODate](
- `type`: [interval type](
- `get startDate`: start date as date, requires [parse()](
- `get endDate`: end date as date, requires [parse()](
Number representing the interval type flags. Available after [parse](
- `1`: Repeat
- `2`: Start date
- `4`: Duration
- `8`: End date
**Example flags**
- `3`: Repeat and start date, rather pointless but possible nevertheless
- `5`: Repeat and duration
- `6`: Start date and duration
- `7`: Repeat, start date, and duration
- `10`: Start- and end date
- `12`: Duration and end date
- `13`: Repeat, duration, and end date
> Do I have repeat in my interval?
```javascript
import { parseInterval } from '@0dep/piso';
console.log((parseInterval('R3/P1Y').type & 1) === 1 ? 'Yes' : 'No');
// Yes
console.log((parseInterval('R-1/P1Y').type & 1) === 1 ? 'Yes' : 'No');
// Yes, indefinite number of repetititions
console.log((parseInterval('R-1/2024-03-27/P1Y').type & 1) === 1 ? 'Yes' : 'No');
// Yes, indefinite number of repetititions from start date
console.log((parseInterval('R-1/P1Y/2024-03-27').type & 1) === 1 ? 'Yes' : 'No');
// Yes, indefinite number of repetititions until end date
console.log((parseInterval('R0/P1Y').type & 1) === 1 ? 'Yes' : 'No');
// No, zero is equal to once
console.log((parseInterval('R1/P1Y').type & 1) === 1 ? 'Yes' : 'No');
// No, since it's just once
console.log((parseInterval('R1/2024-03-28').type & 1) === 1 ? 'Yes' : 'No');
// No, pointless repeat
console.log((parseInterval('R1/2024-03-28/31').type & 1) === 1 ? 'Yes' : 'No');
// No, pointless repeat
console.log((parseInterval('R1/P1Y/2024-03-28').type & 1) === 1 ? 'Yes' : 'No');
// No
```
> Is start date defined in my interval?
```javascript
import { parseInterval } from '@0dep/piso';
const interval = parseInterval('R-1/2024-03-28/P1Y');
console.log((interval.type | 2) === interval.type ? 'Yes' : 'No');
```
Returns [ISOInterval](
Throws `RangeError` if something is off.
Get interval represented as JavaScript Object Notation.
```javascript
import { ISOInterval } from '@0dep/piso';
console.log(JSON.stringify({ interval: new ISOInterval('R2/P1Y/2024-03-28') }, null, 2));
```
ISO date instance.
**Constructor**:
- `source`: ISO 8601 date source string
- `options`: optional parsing options
- `offset`: source string offset column number, -1 is default
- `endChars`: string with optional characters that mark the end of the ISO date, e.g. `/`
- `enforceSeparators`: boolean that will require time part separators such as `-` and `:`
- `enforceUTC`: optional boolean, enforce UTC if source lacks time zone offset
**Properties:**
- `result`:
- `Y`: full year
- `M`: javascript month
- `D`: date or ordinal day
- `H`: hours
- `m`: minutes
- `S`: seconds
- `F`: milliseconds
- `Z`: Z, +, −, or -
- `OH`: offset hours
- `Om`: offset minutes
- `OS`: offset seconds
- `isValid`: boolean indicating if parse was successful
Parse partial date as compared to passed date part arguments.
- `Y`: required full year
- `M`: optional javascript month, required is not ordinal day
- `D`: required date, weekday (1 = Monday .. 7 = Sunday) if `W` is passed, or ordinal day
- `W`: optional week number, then `D` is the week day
Returns [ISODate](
Get Date represented by source.
- `enforceUTC`: optional boolean, enforce UTC if source lacks time zone offset
Get Date represented as JavaScript Object Notation.
Duration instance.
**Constructor**:
- `source`: duration source string
- `offset`: optional source string offset column number
**Properties:**
- `result`:
- `Y`: years
- `M`: months
- `W`: weeks
- `D`: days
- `H`: hours
- `m`: minutes
- `S`: seconds
Get duration in milliseconds from optional start date.
Get duration in milliseconds until optional end date.
An example to get start and end date:
```javascript
import { parseInterval } from '@0dep/piso';
const source = '2007-03-01T13:00:00Z/P1Y2M10DT2H30M';
const interval = parseInterval(source);
console.log('starts at', interval.getStartAt());
console.log('expires at', interval.getExpireAt());
console.log('duration milliseconds', interval.duration.toMilliseconds());
```
An example to get duration milliseconds:
```javascript
import { parseDuration } from '@0dep/piso';
const duration = parseDuration('PT2H30M');
console.log('duration millisecods', duration.toMilliseconds(new Date()));
```
`R4/P2Y/2007-08-01`
| Repetition | start at | expire at |
| ---------: | ---------- | ---------- |
| 4 | 1999-08-01 | 2001-08-01 |
| 3 | 2001-08-01 | 2003-08-01 |
| 2 | 2003-08-01 | 2005-08-01 |
| 1 | 2005-08-01 | 2007-08-01 |
Seems to run 3 times more efficient than RegExp implementations. But date parsing is, of course, slower compared to `new Date('2024-03-26')`. On the other hand `new Date('2024-03-26')` resolves to UTC while `new Date(2024, 2, 26)` does not. Not sure what to expect but IMHO `new Date('2024-03-26')` should be a local date.
| Capability | piso | luxon |
| ------------------ | ---- | ----- |
| start/end | ✓ | ✓ |
| start/duration | ✓ | ✓ |
| duration/end | ✓ | ✓ |
| Repeating interval | ✓ | ❌ |
| Relative end date | ✓ | ❌ |
| Capability | piso | iso8601-duration | luxon |
| --------------------------------- | ---- | ---------------- | ----- |
| Fractional time designator | ✓ | ✓ | ✓ |
| Invalid if more than one fraction | ✓ | ✓ | ✓ |
| Fractional date designator | ✓ | ❌ | ✓ |
| Comma as fraction separator | ✓ | ✓ | ❌ |
| Repeated duration instruction | ✓ | ❌\* | ❌ |
> \* ignored by iso8601-duration
| Capability | piso | luxon | node 24 |
| --------------------------- | ---- | ----- | ------- |
| The 24:th hour | ✓ | ✓ | ✓ |
| Year +10000 | ✓ | ✓ | ✓ |
| Year 9999 | ✓ | ✓ | ✓ |
| BC dates | ✓ | ✓ | ✓ |
| Week | ✓ | ✓ | ❌ |
| Ordinal date | ✓ | ✓ | ❌\* |
| Without separators | ✓ | ✓ | ❌ |
| Without offset minutes | ✓ | ✓ | ❌ |
| Comma as fraction separator | ✓ | ✓ | ❌ |
| Throw on invalid leap year | ✓ | ✓ | ❌\*\* |
| Offset unicode minus (−) | ✓ | ❌ | ❌ |
| Offset seconds | ✓ | ❌ | ❌ |
| 36 fractions of a second | ❌ | ❌ | ✓ |
> \* node misinterprets `2024-012` as December and fails when `2024-013` or `2024-012T07:30` is passed<br/>
> \*\* node is benevolent when parsing `2100-02-29` as `2100-03-01`