UNPKG

@silane/datetime

Version:

Date and time library similar to Python's "datetime" package.

1,575 lines (1,511 loc) 62.3 kB
import { NotImplementedDateTimeError, TypeDateTimeError, ValueDateTimeError, } from './errors.js'; /** * The smallest year number allowed in a date or datetime object. */ export const MINYEAR = 1 /** * The largest year number allowed in a date or datetime object. */ export const MAXYEAR = 9999 /** @typedef {globalThis.Date} stdDate */ const stdDate = globalThis.Date; /** * "stdDate.UTC" converts years between 0 and 99 to a year in the 20th century. * Usually it can be avoided just adding setUTCFullYear(year) * after constructing the "stdDate" instance. * Buf if the parameters from month to milliseconds are outside of their * range, year can be updated to accommodate these values. * In this case, this function must be used. * @param {number} year * @param {number} month * @param {number} day * @param {number} hour * @param {number} minute * @param {number} second * @param {number} millisecond */ function safeStdDateUTC(year, month, day, hour, minute, second, millisecond) { const d = new stdDate(2000, 0, 1); d.setUTCFullYear(year, month - 1, day); d.setUTCHours(hour, minute, second, millisecond); return d; } const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] const leapedDaysPerMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] const totalDaysPerMonth = (function() { let sum = 0 const ret = daysPerMonth.map(x => sum += x) ret.unshift(0) return ret })() const totalLeapedDaysPerMonth = (function() { let sum = 0 const ret = leapedDaysPerMonth.map(x => sum += x) ret.unshift(0) return ret })() /** * Try set property value and returns true if successful or false if fails for some reason like object is freezed. * @param {object} obj * @param {string | symbol} prop * @param {unknown} value * @returns {boolean} */ function trySetProperty(obj, prop, value) { try { obj[prop] = value; } catch(e) { if(e instanceof TypeError) { return false; } throw e; } return Object.is(obj[prop], value); } /** * Calculate quotient and remainder. * @param {number} a - Dividend * @param {number} b - Divisor * @returns {[number, number]} Quotient and remainder respectively */ function divmod(a, b) { const quotient = Math.floor(a / b) return [quotient, a - quotient * b] } /** * Number to "0" padded string with given length. * @param {number} integer * @param {number} length * @returns {string} */ function zeroPad(integer, length) { return integer.toString().padStart(length, '0') } /** * TimeDelta to offset string. * @param {TimeDelta} timeDelta - Must be between timedelta({hours: -24}) and * timedelta({hours: 24}) both exclusive. * @returns {string} */ function toOffsetString(timeDelta) { let offset = timeDelta const minus = offset.days < 0 if(minus) { offset = neg(offset) } const seconds = offset.seconds % 60 const totalMinutes = Math.floor(offset.seconds / 60) const minutes = zeroPad(totalMinutes % 60, 2) const hours = zeroPad(Math.floor(totalMinutes / 60), 2) let ret = `${minus ? '-' : '+'}${hours}:${minutes}` if(offset.microseconds) { ret += `:${zeroPad(seconds, 2)}.${zeroPad(offset.microseconds, 6)}` } else if (seconds) { ret += `:${zeroPad(seconds, 2)}` } return ret } /** * Returns if it's leap year. * @param {number} year * @returns {boolean} */ function isLeapYear(year) { if(year % 4 !== 0) return false if(year % 100 === 0 && year % 400 !==0) return false return true } /** * DateTime to formatted string. * @param {DateTime} dt * @param {string} format * @returns {string} */ function strftime(dt, format) { const a = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] const A = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] const b = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] const B = ['January', 'February', 'March', 'April', 'May', 'June','July', 'August', 'September', 'October', 'November', 'December'] let ret = '' for(let i=0; i < format.length; ++i) { if(format[i] !== '%' || i + 1 >= format.length) { ret += format[i] continue } let s switch(format[i + 1]) { case 'a': s = a[dt.weekday()] break case 'A': s = A[dt.weekday()] break case 'w': s = ((dt.weekday() + 1) % 7).toString() break case 'd': s = zeroPad(dt.day, 2) break case 'b': s = b[dt.month - 1] break case 'B': s = B[dt.month - 1] break case 'm': s = zeroPad(dt.month, 2) break case 'y': s = zeroPad(dt.year % 100, 2) break case 'Y': s = zeroPad(dt.year, 4) break case 'H': s = zeroPad(dt.hour, 2) break case 'I': s = zeroPad(dt.hour % 12, 2) break case 'p': s = dt.hour < 12 ? 'AM' : 'PM' break case 'M': s = zeroPad(dt.minute, 2) break case 'S': s = zeroPad(dt.second, 2) break case 'f': s = zeroPad(dt.microsecond, 6) break case 'z': const offset = dt.utcOffset(); if(offset == null) s = ''; else s = toOffsetString(offset).replace(':', ''); break; case 'Z': const tzName = dt.tzName(); if(tzName == null) s = ''; else s = tzName; break; case '%': s = '%' break; } ret += s ++i } return ret } /** @type {?TimeDelta} */ let timeDeltaMin = null; /** @type {?TimeDelta} */ let timeDeltaMax = null; /** @type {?TimeDelta} */ let timeDeltaResolution = null; /** * Represents a duration, the difference between two dates or times. * Javascript version of * https://docs.python.org/3/library/datetime.html#datetime.timedelta. */ export class TimeDelta { /** * @param {Object} duration An object consisting of duration values. * @param {number} [duration.days] * @param {number} [duration.seconds] * @param {number} [duration.microseconds] * @param {number} [duration.milliseconds] * @param {number} [duration.minutes] * @param {number} [duration.hours] * @param {number} [duration.weeks] */ constructor({ days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0, }={}) { microseconds += milliseconds * 1000 seconds += minutes * 60 seconds += hours * 3600 days += weeks * 7 let frac; [days, frac] = divmod(days, 1); seconds += frac * 3600 * 24; [seconds, frac] = divmod(seconds, 1); microseconds += frac * 1000 ** 2; microseconds = Math.round(microseconds); let div, mod; [div, mod] = divmod(microseconds, 1000 ** 2) microseconds = mod seconds += div; [div, mod] = divmod(seconds, 3600 * 24) seconds = mod days += div if(!(-999999999 <= days && days <= 999999999)) { throw new ValueDateTimeError( 'Cannot handle duration greater than "TimeDelta.max" or ' + 'lesser than "TimeDelta.min".' ); } /** * @private * @readonly */ this._days = days /** * @private * @readonly */ this._seconds = seconds /** * @private * @readonly */ this._microseconds = microseconds } /** * Between -999999999 and 999999999 inclusive. * @type {number} */ get days() { return this._days } /** * Between 0 and 86399 inclusive * @type {number} */ get seconds() { return this._seconds } /** * Between 0 and 999999 inclusive * @type {number} */ get microseconds() { return this._microseconds } /** * Return the total number of seconds contained in the duration. * @returns {number} */ totalSeconds() { if(this['_totalSeconds'] != null) return this['_totalSeconds']; const ret = ( this.days * 3600 * 24 + this.seconds + this.microseconds / 1000000 ); trySetProperty(this, '_totalSeconds', ret); return ret; } /** * Return the human-readable string respresentation. * @returns {string} */ toString() { if(this['_string'] != null) return this['_string']; let ret = '' if(this.days) { ret += `${this.days} day(s), ` } const totalMinutes = Math.floor(this.seconds / 60) ret += `${Math.floor(totalMinutes / 60)}:${zeroPad(totalMinutes % 60, 2)}:` + `${zeroPad(this.seconds % 60, 2)}` if(this.microseconds) { ret += `.${zeroPad(this.microseconds, 6)}` } trySetProperty(this, '_string', ret); return ret } /** * Same as totalSeconds(). * @returns {number} */ valueOf() { return this.totalSeconds() } /** * The most negative timedelta object, new TimeDelta(\{days: -999999999\}). * @type {!TimeDelta} */ static get min() { if(!timeDeltaMin) { timeDeltaMin = new TimeDelta({ days: -999999999 }); } return timeDeltaMin; } /** * The most positive timedelta object, new TimeDelta(\{days: 999999999, * hours: 23, minutes: 59, seconds: 59, microseconds: 999999\}). * @type {!TimeDelta} */ static get max() { if(!timeDeltaMax) { timeDeltaMax = new TimeDelta({ days: 999999999, hours: 23, minutes: 59, seconds: 59, microseconds: 999999, }); } return timeDeltaMax; } /** * The smallest possible difference between non-equal timedelta objects, * new TimeDelta(\{microseconds: 1\}). * @type {!TimeDelta} */ static get resolution() { if(!timeDeltaResolution) { timeDeltaResolution = new TimeDelta({ microseconds: 1 }); } return timeDeltaResolution; } } /** @type {?Date} */ let dateMin = null; /** @type {?Date} */ let dateMax = null; /** @type {?TimeDelta} */ let dateResolution = null; /** * Represents a date (year, month and day) in an idealized calendar. * Javascript version of * https://docs.python.org/3/library/datetime.html#datetime.date. */ export class Date { /** * @param {number} year Between MINYEAR and MAXYEAR. * @param {number} month Between 1 and 12. * @param {number} day Between 1 and the number of days in the given month * and year. */ constructor(year, month, day) { if(!(MINYEAR <= year && year <= MAXYEAR)) throw new ValueDateTimeError( '"year" must be between "MINYEAR" and "MAXYEAR".') if(!(1 <= month && month <= 12)) throw new ValueDateTimeError('"month" must be between 1 and 12.') if(!(1 <= day && day <= ( isLeapYear(year) ? leapedDaysPerMonth[month - 1] : daysPerMonth[month - 1] ))) throw new ValueDateTimeError('Invalid day for the year and month.') /** * @private * @readonly */ this._year = year /** * @private * @readonly */ this._month = month /** * @private * @readonly */ this._day = day } /** * Between MINYEAR and MAXYEAR. * @type {number} */ get year() { return this._year } /** * Between 1 and 12. * @type {number} */ get month() { return this._month } /** * Between 1 and the number of days in the given month and year. * @type {number} */ get day() { return this._day } /** * Return the Date corresponding to the given standard library Date object. * @param {!stdDate} d The standard library Date object. * @param {boolean} utc If true, use getUTC***() instead of get***() * to construct Date. * @returns {!Date} */ static fromStdDate(d, utc=false) { if(!utc) return new Date( d.getFullYear(), d.getMonth() + 1, d.getDate()) else return new Date( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()) } /** * Return the current local date. * @returns {!Date} */ static today() { const today = new stdDate() return Date.fromStdDate(today) } /** * Return the Date corresponding to the proleptic Gregorian ordinal, where * January 1 of year 1 has ordinal 1. * @param {number} ordinal The proleptic Gregorian ordinal. * @returns {!Date} */ static fromOrdinal(ordinal) { let q, r; let year = 1 ,month = 1, day = 1; [q, r] = divmod(ordinal - 1, 365 * 303 + 366 * 97); year += q * 400; [q, r] = divmod(r, 365 * 76 + 366 * 24); year += q * 100; [q, r] = divmod(r, 365 * 3 + 366 * 1); year += q * 4; [q, r] = divmod(r, 365); if(q <= 2) { // not a leap year year += q; for(month = 1; month <= 12 && r >= totalDaysPerMonth[month]; ++month); day += r - totalDaysPerMonth[month - 1]; } else { // leap year year += 3; if(q === 4) r += 365; for(month = 1; month <= 12 && r >= totalLeapedDaysPerMonth[month]; ++month); day += r - totalLeapedDaysPerMonth[month - 1]; } return new Date(year, month, day); } /** * Return a Date corresponding to a dateString given in the format * `YYYY-MM-DD` or `YYYYMMDD`. * @param {string} dateString The date string. * @returns {!Date} */ static fromISOFormat(dateString) { let match = (/^(\d\d\d\d)-(\d\d)-(\d\d)$/.exec(dateString) || /^(\d\d\d\d)(\d\d)(\d\d)$/.exec(dateString)); if(match == null) { throw new ValueDateTimeError('Invalid format.'); } const [year, month, day] = match.slice(1).map(Number); return new Date(year, month, day); } /** * Return a standard library Date object corresponding to this Date. * @param {boolean} utc If true, the value of getUTC***(), instead of * get***(), will correspond to this Date. * @returns {!stdDate} */ toStdDate(utc=false) { let ret; if(!utc) { ret = new stdDate(this.year, this.month - 1, this.day); ret.setFullYear(this.year); } else { ret = new stdDate(stdDate.UTC(this.year, this.month - 1, this.day)); ret.setUTCFullYear(this.year); } return ret; } /** * Return a Date with the same value, except for those parameters given new * values by whichever keyword arguments are specified. * @param {Object} newValues The object consisting of new values. * @param {number} [newValues.year] * @param {number} [newValues.month] * @param {number} [newValues.day] * @returns {!Date} */ replace({ year=this.year, month=this.month, day=this.day }) { return new Date(year, month, day); } /** * Return the proleptic Gregorian ordinal of the Date, * where January 1 of year 1 has ordinal 1. * For any Date object d, Date.fromordinal(d.toordinal()) == d. * @returns {number} */ toOrdinal() { if(this['_ordinal'] != null) return this['_ordinal']; let totalDays = 0 const lastYear = this.year - 1 const nLeapYear = Math.floor(lastYear / 4) - Math.floor(lastYear / 100) + Math.floor(lastYear / 400) totalDays += nLeapYear * 366 + (lastYear - nLeapYear) * 365 if(isLeapYear(this.year)) { totalDays += totalLeapedDaysPerMonth[this.month - 1] } else { totalDays += totalDaysPerMonth[this.month - 1] } totalDays += this.day trySetProperty(this, '_ordinal', totalDays); return totalDays } /** * Return the day of the week as an integer, where Monday is 0 and Sunday * is 6. For example, date(2002, 12, 4).weekday() == 2, a Wednesday. * @returns {number} */ weekday() { return (this.toStdDate().getDay() + 6) % 7 } /** * Return a string representing the date in ISO 8601 format, YYYY-MM-DD. * @returns {string} */ isoFormat() { if(this['_isoFormat'] != null) return this['_isoFormat']; const ret = `${zeroPad(this.year, 4)}-${ zeroPad(this.month, 2) }-${zeroPad(this.day, 2)}`; trySetProperty(this, '_isoFormat', ret); return ret; } /** * Return a string representing the date, controlled by an explicit format * string. Format codes referring to hours, minutes or seconds will see 0 * values. * @param {string} format The format string. * @returns {string} */ strftime(format) { const dt = DateTime.combine(this, new Time()); return strftime(dt, format); } /** * Same as isoFormat(). * @returns {string} */ toString() { return this.isoFormat() } /** * Same as toOrdinal(). * @returns {number} */ valueOf() { return this.toOrdinal(); } /** * The earliest representable date, new Date(MINYEAR, 1, 1). * @type {!Date} */ static get min() { if(!dateMin) { dateMin = new Date(MINYEAR, 1, 1); } return dateMin; } /** * The latest representable date, new Date(MAXYEAR, 12, 31). * @type {!Date} */ static get max() { if(!dateMax) { dateMax = new Date(MAXYEAR, 12, 31); } return dateMax; } /** * The smallest possible difference between non-equal date objects, * new TimeDelta(\{days: 1\}). * @type {!TimeDelta} */ static get resolution() { if(!dateResolution) { dateResolution = new TimeDelta({ days: 1 }); } return dateResolution; } } /** * This is an abstract base class, meaning that this class should not be * instantiated directly. Define a subclass of tzinfo to capture information * about a particular time zone. * Javascript version of * https://docs.python.org/3/library/datetime.html#datetime.tzinfo. */ export class TZInfo { /** * Return offset of local time from UTC, as a TimeDelta object that is * positive east of UTC. If local time is west of UTC, this should be * negative. * @param {?DateTime} dt The DateTime object. * @returns {?TimeDelta} */ utcOffset(dt) { throw new NotImplementedDateTimeError() } /** * Return the daylight saving time (DST) adjustment, as a TimeDelta object * or null if DST information isn’t known. * @param {?DateTime} dt The DateTime object. * @returns {?TimeDelta} */ dst(dt) { throw new NotImplementedDateTimeError() } /** * Return the time zone name corresponding to the datetime object dt, as a * string. * @param {?DateTime} dt The DateTime object. * @returns {?string} */ tzName(dt) { throw new NotImplementedDateTimeError() } /** * This is called from the default datetime.astimezone() implementation. * When called from that, dt.tzinfo is self, and dt’s date and time data are * to be viewed as expressing a UTC time. The purpose of fromutc() is to * adjust the date and time data, returning an equivalent datetime in self’s * local time. * @param {!DateTime} dt The DateTime object. * @returns {!DateTime} */ fromUTC(dt) { if(dt.tzInfo !== this) { throw new ValueDateTimeError( '"dt.tzInfo" must be same instance as "this".') } let dtoff = dt.utcOffset() let dtdst = dt.dst() if(dtoff == null || dtdst == null) { throw new ValueDateTimeError( '"dt.utcOffset()" and "dt.dst()" must not return null.') } const delta = sub(dtoff, dtdst) if(cmp(delta, new TimeDelta()) !== 0) { dt = add(dt, delta) dtdst = dt.dst() } if(dtdst == null) return dt else return add(dt, dtdst) } } /** @type {?TimeZone} */ let timeZoneUTC = null; /** * The TimeZone class is a subclass of TZInfo, each instance of which represents * a timezone defined by a fixed offset from UTC. * Objects of this class cannot be used to represent timezone information in the * locations where different offsets are used in different days of the year or * where historical changes have been made to civil time. * Javascript version of * https://docs.python.org/3/library/datetime.html#datetime.timezone. */ export class TimeZone extends TZInfo { /** * * @param {!TimeDelta} offset Represents the difference between the local * time and UTC. It must be strictly between * -TimeDelta(\{hours: 24\}) and * TimeDelta(\{hours: 24\}), otherwise * ValueDateTimeError is raised. * @param {?string} name If specified, it must be a string that will be used * as the value returned by the DateTime.tzname() * method. */ constructor(offset, name=null) { super() if(!(cmp(new TimeDelta({hours: -24}), offset) < 0 && cmp(offset, new TimeDelta({hours: 24})) < 0)) throw new ValueDateTimeError( '"offset" must be "TimeDelta({hours: -24}) < offset < ' + 'TimeDelta({hours: 24})".') if(name == null) { name = 'UTC' if(cmp(offset, new TimeDelta()) != 0 ) { name += toOffsetString(offset) } } /** * @private * @readonly */ this._offset = offset /** * @private * @readonly */ this._name = name } /** * Return the fixed value specified when the TimeZone instance is * constructed. * @param {?DateTime} dt This argument is ignored. * @returns {!TimeDelta} */ utcOffset(dt) { return this._offset } /** * Return the fixed value specified when the timezone instance is * constructed. * If name is not provided in the constructor, the name returned by * tzName(dt) is generated from the value of the offset as follows. * If offset is TimeDelta(\{\}), the name is “UTC”, otherwise it is a string * in the format UTC±HH:MM, where ± is the sign of offset, HH and MM are two * digits of offset.hours and offset.minutes respectively. * @param {?DateTime} dt This argument is ignored. * @returns {string} */ tzName(dt) { return this._name } /** * Always returns null. * @param {?DateTime} dt This argument is ignored. * @returns {null} */ dst(dt) { return null } /** * Return add(dt, offset). The dt argument must be an aware datetime * instance, with tzInfo set to this. * @param {!DateTime} dt The DateTime object. * @returns {!DateTime} */ fromUTC(dt) { if(dt.tzInfo !== this) { throw new ValueDateTimeError( '"dt.tzInfo" must be same instance as "this".') } return add(dt, this._offset) } /** * The UTC timezone, new TimeZone(new TimeDelta(\{\})). * @type {!TimeZone} */ static get utc() { if(!timeZoneUTC) { timeZoneUTC = new TimeZone(new TimeDelta({})); } return timeZoneUTC; } } /** * A subclass of TZInfo representing local timezone of execution environment. */ class LocalTZInfo extends TZInfo { constructor() { super() // Offset without DST const stdOffset = -new stdDate(2000, 0, 1).getTimezoneOffset() /** * @private * @readonly */ this._stdOffset = new TimeDelta({minutes: stdOffset}) } /** * Return offset of local time from UTC, as a TimeDelta object that is * positive east of UTC. If local time is west of UTC, this is negative. * @param {?DateTime} dt The DateTime object. * @returns {!TimeDelta} */ utcOffset(dt) { if(dt == null) return this._stdOffset const offset = -dt.toStdDate(false).getTimezoneOffset() return new TimeDelta({minutes: offset}) } /** * Return the daylight saving time (DST) adjustment as a TimeDelta object. * @param {?DateTime} dt The DateTime object. * @returns {!TimeDelta} */ dst(dt) { if(dt == null) return new TimeDelta() const offsetMinutes = -dt.toStdDate(false).getTimezoneOffset() const offset = new TimeDelta({minutes: offsetMinutes}) return sub(offset, this._stdOffset) } /** * Return the time zone name corresponding to the datetime object dt, as a * string. * @param {?DateTime} dt The DateTime object. * @returns {string} */ tzName(dt) { const offset = this.utcOffset(dt) return toOffsetString(offset) } /** * This is called from the default datetime.astimezone() implementation. * When called from that, dt.tzinfo is self, and dt’s date and time data are * to be viewed as expressing a UTC time. The purpose of fromutc() is to * adjust the date and time data, returning an equivalent datetime in self’s * local time. * @param {!DateTime} dt The DateTime object. * @returns {!DateTime} */ fromUTC(dt) { if(dt.tzInfo !== this) throw new ValueDateTimeError( '"dt.tzInfo" must be same instance as "this".') const local = DateTime.fromStdDate(dt.toStdDate(true), false).replace({ microsecond: dt.microsecond, tzInfo: this, fold: 0}) return local } } /** * An instance of a class which is a subclass of TZInfo representing local * timezone of execution environment. */ export const LOCALTZINFO = new LocalTZInfo() /** @type {?Time} */ let timeMin = null; /** @type {?Time} */ let timeMax = null; /** @type {?TimeDelta} */ let timeResolution = null; /** * A time object represents a (local) time of day, independent of any particular * day, and subject to adjustment via a tzinfo object. * Javascript version of * https://docs.python.org/3/library/datetime.html#datetime.time. */ export class Time { /** * @param {number} hour Between 0 and 23. * @param {number} minute Between 0 and 59. * @param {number} second Between 0 and 59. * @param {number} microsecond Between 0 and 999999. * @param {?TZInfo} tzInfo The timezone information. * @param {number} fold 0 or 1. */ constructor(hour=0, minute=0, second=0, microsecond=0, tzInfo=null, fold=0) { if(!(0 <= hour && hour <= 23)) throw new ValueDateTimeError( '"hour" must be between 0 and 23.' ); if(!(0 <= minute && minute <= 59)) throw new ValueDateTimeError( '"minute" must be between 0 and 59.' ); if(!(0 <= second && second <= 59)) throw new ValueDateTimeError( '"second" must be between 0 and 59.' ); if(!(0 <= microsecond && microsecond <= 999999)) throw new ValueDateTimeError( '"microsecond" must be between 0 and 999999.' ); if(!(fold === 0 || fold === 1)) throw new ValueDateTimeError( '"fold" must be 0 or 1.' ); /** * @private * @readonly */ this._hour = hour; /** * @private * @readonly */ this._minute = minute; /** * @private * @readonly */ this._second = second; /** * @private * @readonly */ this._microsecond = microsecond; /** * @private * @readonly */ this._tzInfo = tzInfo; /** * @private * @readonly */ this._fold = fold; } /** * Between 0 and 23. * @type {number} */ get hour() { return this._hour } /** * Between 0 and 59. * @type {number} */ get minute() { return this._minute } /** * Between 0 and 59. * @type {number} */ get second() { return this._second } /** * Between 0 and 999999. * @type {number} */ get microsecond() { return this._microsecond } /** * The object passed as the tzInfo argument to the Time constructor, or null * if none was passed. * @type {?TZInfo} */ get tzInfo() { return this._tzInfo } /** * 0 or 1. Used to disambiguate wall times during a repeated interval. * (A repeated interval occurs when clocks are rolled back at the end of * daylight saving time or when the UTC offset for the current zone is * decreased for political reasons.) The value 0 (1) represents the earlier * (later) of the two moments with the same wall time representation. * @type {number} */ get fold() { return this._fold } /** * Return a Time corresponding to a dateString given in the format * `HH[:MM[:SS[.fff[fff]]]][Z|((+|-)HH[:MM[:SS[.fff[fff]]]])]` or * `HH[MM[SS[.fff[fff]]]][Z|((+|-)HH[MM[SS[.fff[fff]]]])]`. * @param {string} timeString The time string. * @returns {!Time} */ static fromISOFormat(timeString) { function parseTimeString(str) { const match = ( /^(\d\d)(?:\:(\d\d)(?:\:(\d\d)(?:\.(\d{3})(\d{3})?)?)?)?$/.exec(str) || /^(\d\d)(?:(\d\d)(?:(\d\d)(?:\.(\d{3})(\d{3})?)?)?)?$/.exec(str) ) if(match == null) return null match.splice(0, 1) const ret = match.map(x => x == null ? 0 : parseInt(x, 10)) ret[3] = ret[3] * 1000 + ret[4] ret.splice(4, 1) return ret } let sepIdx = timeString.search(/[Z+-]/) if(sepIdx === -1) sepIdx = timeString.length const timeStr = timeString.slice(0, sepIdx) const offsetStr = timeString.slice(sepIdx) const timeArray = parseTimeString(timeStr) if(timeArray == null) throw new ValueDateTimeError( 'Invalid format.') let tzInfo = null if(offsetStr === 'Z') { tzInfo = new TimeZone(new TimeDelta({})); } else if(offsetStr !== '') { const offsetArray = parseTimeString(offsetStr.slice(1)) if(offsetArray == null) { throw new ValueDateTimeError( 'Invalid format.') } let offset = new TimeDelta({ hours: offsetArray[0], minutes: offsetArray[1], seconds: offsetArray[2], microseconds: offsetArray[3], }) if(offsetStr[0] === '-') offset = neg(offset) tzInfo = new TimeZone(offset) } return new Time( timeArray[0], timeArray[1], timeArray[2], timeArray[3], tzInfo) } /** * Return a time with the same value, except for those attributes given new * values by whichever keyword arguments are specified. Note that * \{tzinfo: null\} can be specified to create a naive time from an aware * time, without conversion of the time data. * @param {Object} newValues The object consisting of new values. * @param {number} [newValues.hour] * @param {number} [newValues.minute] * @param {number} [newValues.second] * @param {number} [newValues.microsecond] * @param {?TZInfo} [newValues.tzInfo] * @param {number} [newValues.fold] * @returns {!Time} */ replace({ hour=this.hour, minute=this.minute, second=this.second, microsecond=this.microsecond, tzInfo=this.tzInfo, fold=this.fold }) { return new Time(hour, minute, second, microsecond, tzInfo, fold) } /** * Return a string representing the time in ISO 8601 format. * @param {"auto"|"microseconds"|"milliseconds"|"seconds"|"minutes"|"hours" * } timeSpec Specifies the number of additional components of the time to * include. * @returns {string} */ isoFormat(timeSpec='auto') { if(timeSpec === 'auto') { timeSpec = this.microsecond ? 'microseconds' : 'seconds' } if(this['_isoFormat']?.[timeSpec] != null) { return this['_isoFormat'][timeSpec]; } let ret = '' switch(timeSpec) { case 'microseconds': case 'milliseconds': if(timeSpec === 'microseconds') ret = zeroPad(this.microsecond, 6) + ret else ret = zeroPad(Math.floor(this.microsecond / 1000), 3) + ret ret = '.' + ret case 'seconds': ret = ':' + zeroPad(this.second, 2) + ret case 'minutes': ret = ':' + zeroPad(this.minute, 2) + ret case 'hours': ret = zeroPad(this.hour, 2) + ret break default: throw new ValueDateTimeError( '"timeSpec" must be either "auto", "microseconds", "milliseconds", ' + '"seconds", "minutes" or "hours".') } const offset = this.utcOffset() if(offset != null) { ret += toOffsetString(offset) } if(!this['_isoFormat']) { trySetProperty(this, '_isoFormat', {}); } // Suppose the object is freezed by is if(this['_isoFormat']) { trySetProperty(this['_isoFormat'], timeSpec, ret); } return ret } /** * If tzInfo is null, returns null, else returns this.tzInfo.utcOffset(null). * @returns {?TimeDelta} */ utcOffset() { return this.tzInfo == null ? null : this.tzInfo.utcOffset(null) } /** * If tzInfo is null, returns null, else returns this.tzInfo.dst(null). * @returns {?TimeDelta} */ dst() { return this.tzInfo == null ? null : this.tzInfo.dst(null) } /** * If tzInfo is null, returns null, else returns this.tzInfo.tzName(null). * @returns {?string} */ tzName() { return this.tzInfo == null ? null : this.tzInfo.tzName(null) } /** * Return a string representing the time, controlled by an explicit format * string. * @param {string} format The format string. * @returns {string} */ strftime(format) { const dt = DateTime.combine(new Date(1900, 1, 1), this); return strftime(dt, format); } /** * Same as isoFormat(). * @returns {string} */ toString() { return this.isoFormat() } /** * The earliest representable time, new Time(0, 0, 0, 0). * @type {!Time} */ static get min() { if(!timeMin) { timeMin = new Time(0, 0, 0, 0); } return timeMin; } /** * The latest representable time, new Time(23, 59, 59, 999999). * @type {!Time} */ static get max() { if(!timeMax) { timeMax = new Time(23, 59, 59, 999999); } return timeMax; } /** * The smallest possible difference between non-equal time objects, * new TimeDelta(\{microseconds: 1\}). * @type {!TimeDelta} */ static get resolution() { if(!timeResolution) { timeResolution = new TimeDelta({ microseconds: 1 }); } return timeResolution; } } /** @type {?DateTime} */ let dateTimeMin = null; /** @type {?DateTime} */ let dateTimeMax = null; /** @type {?TimeDelta} */ let dateTimeResolution = null; /** * A DateTime object is a single object containing all the information from a * Date object and a Time object. * Javascript version of * https://docs.python.org/3/library/datetime.html#datetime.datetime. */ export class DateTime extends Date { /** * @param {number} year Between MINYEAR and MAXYEAR. * @param {number} month Between 1 and 12. * @param {number} day Between 1 and the number of days in the given month * and year. * @param {number} hour Between 0 and 23. * @param {number} minute Between 0 and 59. * @param {number} second Between 0 and 59. * @param {number} microsecond Between 0 and 999999. * @param {?TZInfo} tzInfo The timezone information. * @param {number} fold 0 or 1. */ constructor(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzInfo=null, fold=0) { super(year, month, day); if(!(0 <= hour && hour <= 23)) throw new ValueDateTimeError( '"hour" must be between 0 and 23.' ); if(!(0 <= minute && minute <= 59)) throw new ValueDateTimeError( '"minute" must be between 0 and 59.' ); if(!(0 <= second && second <= 59)) throw new ValueDateTimeError( '"second" must be between 0 and 59.' ); if(!(0 <= microsecond && microsecond <= 999999)) throw new ValueDateTimeError( '"microsecond" must be between 0 and 999999.' ); if(!(fold === 0 || fold === 1)) throw new ValueDateTimeError( '"fold" must be 0 or 1.' ); /** * @private * @readonly */ this._hour = hour; /** * @private * @readonly */ this._minute = minute; /** * @private * @readonly */ this._second = second; /** * @private * @readonly */ this._microsecond = microsecond; /** * @private * @readonly */ this._tzInfo = tzInfo; /** * @private * @readonly */ this._fold = fold; } /** * Between 0 and 23. * @type {number} */ get hour() { return this._hour } /** * Between 0 and 59. * @type {number} */ get minute() { return this._minute } /** * Between 0 and 59. * @type {number} */ get second() { return this._second } /** * Between 0 and 999999. * @type {number} */ get microsecond() { return this._microsecond } /** * The object passed as the tzInfo argument to the Time constructor, or null * if none was passed. * @type {?TZInfo} */ get tzInfo() { return this._tzInfo } /** * 0 or 1. Used to disambiguate wall times during a repeated interval. * (A repeated interval occurs when clocks are rolled back at the end of * daylight saving time or when the UTC offset for the current zone is * decreased for political reasons.) The value 0 (1) represents the earlier * (later) of the two moments with the same wall time representation. * @type {number} */ get fold() { return this._fold } /** * Return a DateTime corresponding to the given standard library Date object. * @param {!stdDate} d The standard library Date object. * @param {boolean} utc If true, use getUTC***() instead of get***() * to construct DateTime. * @returns {!DateTime} */ static fromStdDate(d, utc=false) { if(!utc) return new DateTime( d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds() * 1000) else return new DateTime( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds() * 1000) } /** * Return the current local date and time, with tzInfo null. * @returns {!DateTime} */ static today() { return DateTime.fromStdDate(new stdDate()) } /** * Return the current date and time. * @param {?TZInfo} tz If specified, the current date and time are converted * to tz's time zone, else same as today(). * @returns {!DateTime} */ static now(tz=null) { if(tz == null) return DateTime.today() return tz.fromUTC(DateTime.utcNow().replace({tzInfo: tz})) } /** * Return the current UTC date and time, with tzInfo null. * @returns {!DateTime} */ static utcNow() { return DateTime.fromStdDate(new stdDate(), true) } /** * Return the local date and time corresponding to the POSIX timestamp. * @param {number} timeStamp The POSIX timestamp. * @param {?TZInfo} tz If null, the timestamp is converted to the platform's * local date and time, and the returned DateTime object * is naive. If not null, the timestamp is converted to * tz's time zone. * @returns {!DateTime} */ static fromTimeStamp(timeStamp, tz=null) { if(tz == null) return DateTime.fromStdDate(new stdDate(timeStamp * 1000)) return tz.fromUTC( DateTime.utcFromTimeStamp(timeStamp).replace({tzInfo: tz})) } /** * Return the UTC date and time corresponding to the POSIX timestamp, with * tzInfo null. (The resulting object is naive.) * @param {number} timeStamp The POSIX timestamp. * @returns {!DateTime} */ static utcFromTimeStamp(timeStamp) { return DateTime.fromStdDate(new stdDate(timeStamp * 1000), true) } /** * Return a new DateTime object whose date components are equal to the given * Date object’s, and whose time components are equal to the given Time * object’s. If the tzInfo argument is provided, its value is used to set * the tzInfo attribute of the result, otherwise the tzInfo attribute of the * time argument is used. * @param {!Date} date The Date object. * @param {!Time} time The Time object. * @param {?TZInfo} [tzInfo] The TZInfo object. * @returns {!DateTime} */ static combine(date, time, tzInfo=undefined) { if(tzInfo === undefined) tzInfo = time.tzInfo return new DateTime( date.year, date.month, date.day, time.hour, time.minute, time.second, time.microsecond, tzInfo, time.fold) } /** * Return a DateTime corresponding to a dateString in one of the formats * emitted by Date.isoFormat() and DateTime.isoFormat(). * @param {string} dateString The date string. * @returns {!DateTime} */ static fromISOFormat(dateString) { let sepIdx = dateString.search(/[^\d\-]/); if(sepIdx === -1) sepIdx = dateString.length; const dateStr = dateString.slice(0, sepIdx); const timeStr = dateString.slice(sepIdx + 1); return DateTime.combine( Date.fromISOFormat(dateStr), timeStr ? Time.fromISOFormat(timeStr) : new Time(), ); } /** * Return a standard library Date object corresponding to this DateTime. * Since standard library Date object has only millisecond resolution, the * microsecond value is truncated. * @param {boolean} utc If true, the value of getUTC***(), instead of * get***(), will correspond to this Date. * @returns {!stdDate} */ toStdDate(utc=false) { let ret; if(!utc) { ret = new stdDate( this.year, this.month - 1, this.day, this.hour, this.minute, this.second, this.microsecond / 1000); ret.setFullYear(this.year); } else { ret = new stdDate(stdDate.UTC( this.year, this.month - 1, this.day, this.hour, this.minute, this.second, this.microsecond / 1000)); ret.setUTCFullYear(this.year); } return ret; } /** * Return Date object with same year, month and day. * @returns {!Date} */ date() { if(this['_date'] != null) return this['_date']; const ret = new Date(this.year, this.month, this.day); trySetProperty(this, '_date', ret); return ret; } /** * Return Time object with same hour, minute, second, microsecond and fold. * tzInfo is null. * @returns {!Time} */ time() { if(this['_time'] != null) return this['_time']; const ret = new Time( this.hour, this.minute, this.second, this.microsecond, null, this.fold ); trySetProperty(this, '_time', ret); return ret; } /** * Return Time object with same hour, minute, second, microsecond, fold, and * tzInfo attributes. * @returns {!Time} */ timetz() { if(this['_timetz'] != null) return this['_timetz']; const ret = new Time( this.hour, this.minute, this.second, this.microsecond, this.tzInfo, this.fold ); trySetProperty(this, '_timetz', ret); return ret; } /** * Return a DateTime with the same attributes, except for those attributes * given new values by whichever keyword arguments are specified. Note that * \{tzInfo: null\} can be specified to create a naive DateTime from an * aware DateTime with no conversion of date and time data. * @param {Object} newValues The object consisting of new values. * @param {number} [newValues.year] * @param {number} [newValues.month] * @param {number} [newValues.day] * @param {number} [newValues.hour] * @param {number} [newValues.minute] * @param {number} [newValues.second] * @param {number} [newValues.microsecond] * @param {?TZInfo} [newValues.tzInfo] * @param {number} [newValues.fold] * @returns {!DateTime} */ replace({ year=this.year, month=this.month, day=this.day, hour=this.hour, minute=this.minute, second=this.second, microsecond=this.microsecond, tzInfo=this.tzInfo, fold=this.fold, }) { return new DateTime(year, month, day, hour, minute, second, microsecond, tzInfo, fold) } /** * Return a DateTime object with new tzInfo attribute tz, adjusting the date * and time data so the result is the same UTC time as self, but in tz’s * local time. * If self is naive, it is presumed to represent time in the system timezone. * @param {?TZInfo} tz The target timezone. If null, the system local * timezone is assumed for the target timezone. * @returns {!DateTime} */ asTimeZone(tz=null) { if(this.tzInfo === tz) return this const offset = this.utcOffset() if(offset == null && tz == null) return this let utc if(offset == null) { const local = this.replace({tzInfo: LOCALTZINFO}); utc = sub(local, /** @type {TimeDelta} */(local.utcOffset())) } else { utc = sub(this, offset) } const tmpTZ = tz != null ? tz : LOCALTZINFO; const ret = tmpTZ.fromUTC(utc.replace({tzInfo: tmpTZ})) return ret.replace({tzInfo: tz}); } /** * If tzInfo is null, returns null, else returns this.tzInfo.utcOffset(this). * @returns {?TimeDelta} */ utcOffset() { return this.tzInfo == null ? null : this.tzInfo.utcOffset(this) } /** * If tzInfo is null, returns null, else returns this.tzInfo.dst(this). * @returns {?TimeDelta} */ dst() { return this.tzInfo == null ? null : this.tzInfo.dst(this) } /** * If tzInfo is null, returns null, else returns this.tzInfo.tzName(this). * @returns {?string} */ tzName() { return this.tzInfo == null ? null : this.tzInfo.tzName(this) } /** * Return POSIX timestamp corresponding to the DateTime instance. * @returns {number} */ timeStamp() { if(this['_timestamp'] != null) return this['_timestamp']; /** @type {DateTime} */ let dt = this if(this.utcOffset() == null) { dt = this.replace({tzInfo: LOCALTZINFO}) } const ret = sub(dt, new DateTime( 1970, 1, 1, 0, 0, 0, 0, TimeZone.utc )).totalSeconds(); trySetProperty(this, '_timestamp', ret); return ret; } /** * Return a string representing the date and time in ISO 8601 format. * @param {string} sep One-character separator placed between the date and *