An open API service indexing awesome lists of open source software.

https://github.com/zerodep/piso

ISO 8601 date, duration, and interval parser
https://github.com/zerodep/piso

date duration interval iso8601 isomorphic javascript parser

Last synced: 2 months ago
JSON representation

ISO 8601 date, duration, and interval parser

Awesome Lists containing this project

README

          

# piso

[![Build](https://github.com/zerodep/piso/actions/workflows/build.yaml/badge.svg)](https://github.com/zerodep/piso/actions/workflows/build.yaml)[![Coverage Status](https://coveralls.io/repos/github/zerodep/piso/badge.svg?branch=main)](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.

## Contents

- [Api](#api)
- [`parseInterval(iso8601Interval[, enforceUTC])`](#parseintervaliso8601interval-enforceutc)
- [`parseDuration(iso8601Duration)`](#parsedurationiso8601duration)
- [`getDate(iso8601Date[, enforceUTC])`](#getdateiso8601date-enforceutc)
- [`getISOWeekString([date])`](#getisoweekstringdate)
- [`getUTCWeekNumber([date])`](#getutcweeknumberdate)
- [`getUTCLastWeekOfYear(Y)`](#getutclastweekofyeary)
- [`getUTCWeekOneDate(Y)`](#getutcweekonedatey)

## Api

### `parseInterval(iso8601Interval[, enforceUTC])`

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](#new-isointervalsource-enforceutc).

```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() });
}
```

### `parseDuration(iso8601Duration)`

Parse duration from an ISO 8601 duration string.

- `iso8601Duration`: string with ISO 8601 duration source

Returns [ISODuration](#new-isodurationsource-offset).

```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 });
}
```

### `getDate(iso8601Date[, enforceUTC])`

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.

### `getUTCLastWeekOfYear(Y)`

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));
```

### `getUTCWeekOneDate(Y)`

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));
```

### `getISOWeekString([date])`

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)));
```

### `getUTCWeekNumber([date])`

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)));
```

## `new ISOInterval(source[, enforceUTC])`

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](#new-isodatesource-options)
- `duration`: duration as [ISODuration](#new-isodurationsource-offset)
- `end`: end date as [ISODate](#new-isodatesource-options)
- `type`: [interval type](#intervaltype)
- `get startDate`: start date as date, requires [parse()](#intervalparse) to be called
- `get endDate`: end date as date, requires [parse()](#intervalparse) to be called

### `interval.type`

Number representing the interval type flags. Available after [parse](#intervalparse).

- `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');
```

### `interval.parse()`

Returns [ISOInterval](#new-isointervalsource-enforceutc).

Throws `RangeError` if something is off.

### `interval.toJSON()`

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));
```

## `new ISODate(source[, options])`

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

### `date.parse()`

### `date.parsePartialDate(Y, M, D, W)`

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](#new-isodatesource-options)

### `date.toDate([enforceUTC])`

Get Date represented by source.

- `enforceUTC`: optional boolean, enforce UTC if source lacks time zone offset

### `date.toJSON()`

Get Date represented as JavaScript Object Notation.

## `new ISODuration(source[, offset])`

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

### `duration.toMilliseconds([startDate])`

Get duration in milliseconds from optional start date.

### `duration.untilMilliseconds([endDate])`

Get duration in milliseconds until optional end date.

## Example

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()));
```

## Repetitions

### With end 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 |

## Benchmarking

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.

### Interval

| Capability | piso | luxon |
| ------------------ | ---- | ----- |
| start/end | ✓ | ✓ |
| start/duration | ✓ | ✓ |
| duration/end | ✓ | ✓ |
| Repeating interval | ✓ | ❌ |
| Relative end date | ✓ | ❌ |

### Duration

| Capability | piso | iso8601-duration | luxon | [temporal](https://www.npmjs.com/package/@js-temporal/polyfill) |
| --------------------------------- | ---- | ---------------- | ----- | --------------------------------------------------------------- |
| Fractional time designator | ✓ | ✓ | ✓ | ✓ |
| Invalid if more than one fraction | ✓ | ✓ | ✓ | ✓ |
| Year designator | ✓ | ✓ | ✓ | ❌ |
| Fractional date designator | ✓ | ❌ | ✓ | ❌ |
| Comma as fraction separator | ✓ | ✓ | ❌ | ✓ |
| Repeated duration instruction | ✓ | ❌\* | ❌ | ❌ |

> \* ignored

### Date

| 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

> \*\* node is benevolent when parsing `2100-02-29` as `2100-03-01`