UNPKG

@0dep/piso

Version:

ISO 8601 interval, date, and duration parser

498 lines (358 loc) 13.5 kB
# 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 | | --------------------------------- | ---- | ---------------- | ----- | | Fractional time designator | ✓ | ✓ | ✓ | | Invalid if more than one fraction | ✓ | ✓ | ✓ | | Fractional date designator | ✓ | ❌ | ✓ | | Comma as fraction separator | ✓ | ✓ | ❌ | | Repeated duration instruction | ✓ | ❌\* | ❌ | > \* ignored by iso8601-duration ### 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<br/> > \*\* node is benevolent when parsing `2100-02-29` as `2100-03-01`