UNPKG

chronoshift

Version:

A tiny library for shifting time with timezones

320 lines (292 loc) 7.97 kB
/* * Copyright 2015-2019 Imply Data, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */ /* eslint-disable no-useless-escape */ import { fromDate } from '@internationalized/date'; import { Duration } from '../duration/duration'; import { Timezone } from '../timezone/timezone'; function parseYear(v: string): number { if (v.length === 2) { const vn = parseInt(v, 10); return (vn < 70 ? 2000 : 1900) + vn; } else if (v.length === 4) { return parseInt(v, 10); } else { throw new Error('Invalid year in date'); } } function parseMonth(v: string): number { const vn = parseInt(v, 10); if (vn <= 0 || 12 < vn) throw new Error('Invalid month in date'); return vn - 1; } function parseDay(v: string): number { const vn = parseInt(v, 10); if (vn <= 0 || 31 < vn) throw new Error('Invalid day in date'); return vn; } function parseHour(v: string): number { const vn = parseInt(v, 10); if (vn < 0 || 24 < vn) throw new Error('Invalid hour in date'); return vn; } function parseMinute(v: string): number { const vn = parseInt(v, 10); if (vn < 0 || 60 < vn) throw new Error('Invalid minute in date'); return vn; } function parseSecond(v: string): number { const vn = parseInt(v, 10); if (vn < 0 || 60 < vn) throw new Error('Invalid second in date'); return vn; } function parseMillisecond(v: string): number { if (!v) return 0; return parseInt(v.substr(0, 3), 10); } export function parseSQLDate(type: string, v: string): Date { if (type === 't') throw new Error('time literals are not supported'); let m: RegExpMatchArray | null; let d: number; if (type === 'ts') { if ((m = /^(\d{2}(?:\d{2})?)(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(v))) { d = Date.UTC( parseYear(m[1]), parseMonth(m[2]), parseDay(m[3]), parseHour(m[4]), parseMinute(m[5]), parseSecond(m[6]), ); } else if ( (m = /^(\d{2}(?:\d{2})?)[~!@#$%^&*()_+=:.\-\/](\d{1,2})[~!@#$%^&*()_+=:.\-\/](\d{1,2})[T ](\d{1,2})[~!@#$%^&*()_+=:.\-\/](\d{1,2})[~!@#$%^&*()_+=:.\-\/](\d{1,2})(?:\.(\d{1,6}))?$/.exec( v, )) ) { d = Date.UTC( parseYear(m[1]), parseMonth(m[2]), parseDay(m[3]), parseHour(m[4]), parseMinute(m[5]), parseSecond(m[6]), parseMillisecond(m[7]), ); } else { throw new Error('Invalid timestamp'); } } else { if ((m = /^(\d{2}(?:\d{2})?)(\d{2})(\d{2})$/.exec(v))) { d = Date.UTC(parseYear(m[1]), parseMonth(m[2]), parseDay(m[3])); } else if ( (m = /^(\d{2}(?:\d{2})?)[~!@#$%^&*()_+=:.\-\/](\d{1,2})[~!@#$%^&*()_+=:.\-\/](\d{1,2})$/.exec( v, )) ) { d = Date.UTC(parseYear(m[1]), parseMonth(m[2]), parseDay(m[3])); } else { throw new Error('Invalid date'); } } return new Date(d); } // Taken from: https://github.com/csnover/js-iso8601/blob/lax/iso8601.js const numericKeys = [1, 4, 5, 6, 10, 11]; export function parseISODate( date: string, timezone: Timezone | null = Timezone.UTC, ): Date | undefined { let struct: any; let minutesOffset = 0; /* ( \d{4} | [+\-] \d{6} ) (?: -? (\d{2}) (?: -? (\d{2}) )? )? (?: [ T]? (\d{2}) (?: :? (\d{2}) (?: :? (\d{2}) (?: [,\.] (\d{1,}) )? )? )? )? (?: (Z) | ([+\-]) (\d{2}) (?: :? (\d{2}) )? )? */ // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm if ( (struct = /^(\d{4}|[+\-]\d{6})(?:-?(\d{2})(?:-?(\d{2}))?)?(?:[ T]?(\d{2})(?::?(\d{2})(?::?(\d{2})(?:[,\.](\d{1,}))?)?)?)?(?:(Z)|([+\-])(\d{2})(?::?(\d{2}))?)?$/.exec( date, )) ) { // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC for (let i = 0, k: number; (k = numericKeys[i]); ++i) { struct[k] = +struct[k] || 0; } // allow undefined days and months struct[2] = (+struct[2] || 1) - 1; struct[3] = +struct[3] || 1; // allow arbitrary sub-second precision beyond milliseconds struct[7] = struct[7] ? +(struct[7] + '00').slice(0, 3) : 0; if ( (struct[8] === undefined || struct[8] === '') && (struct[9] === undefined || struct[9] === '') && !Timezone.UTC.equals(timezone || undefined) ) { if (timezone === null) { // timezone explicitly set to null = use local timezone return new Date( struct[1], struct[2], struct[3], struct[4], struct[5], struct[6], struct[7], ); } else { const dt = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5], struct[6], struct[7], ); const tzd = fromDate(new Date(dt), timezone.toString()); return new Date(dt - tzd.offset); } } else { if (struct[8] !== 'Z' && struct[9] !== undefined) { minutesOffset = struct[10] * 60 + struct[11]; if (struct[9] === '+') { minutesOffset = 0 - minutesOffset; } } return new Date( Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7], ), ); } } else { return; } } export interface IntervalParse { computedStart: Date; computedEnd: Date; start?: Date; end?: Date; duration?: Duration; } export function parseInterval( str: string, timezone = Timezone.UTC, now = new Date(), ): IntervalParse { const parts = str.split('/'); if (parts.length > 2) throw new Error(`Can not parse string ${str}`); let start: Date | undefined; let end: Date | undefined; let duration: Duration | undefined; const p0: string = parts[0]; if (parts.length === 1) { duration = Duration.fromJS(p0); } else { const p1 = parts[1]; if (p0[0] === 'P') { duration = Duration.fromJS(p0); end = parseISODate(p1, timezone); if (!end) throw new Error(`can not parse '${p1}' as ISO date`); } else if (p1[0] === 'P') { start = parseISODate(p0, timezone); if (!start) throw new Error(`can not parse '${p0}' as ISO date`); duration = Duration.fromJS(p1); } else { start = parseISODate(p0, timezone); if (!start) throw new Error(`can not parse '${p0}' as ISO date`); end = parseISODate(p1, timezone); if (!end) throw new Error(`can not parse '${p1}' as ISO date`); if (end < start) { throw new Error(`start must be <= end in '${str}'`); } } } /* Has to be one of: <start>/<end> <start>/<duration> <duration>/<end> <duration> */ let computedStart: Date; let computedEnd: Date; if (start) { computedStart = start; if (duration) { computedEnd = duration.shift(computedStart, timezone, 1); } else { computedEnd = end!; } } else { computedEnd = end || now; computedStart = duration!.shift(computedEnd, timezone, -1); } return { computedStart, computedEnd, start, end, duration, }; }