/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch */ import Timezone from "./timezone.js"; import Duration from "./duration.js"; import Period from "./period.js"; import design from "./design.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import Property from "./property.js"; import TimezoneService from "./timezone_service.js"; import { strictParseInt, trunc, pad2 } from "./helpers.js"; /** * This lets typescript resolve our custom types in the * generated d.ts files (jsdoc typedefs are converted to typescript types). * Ignore prevents the typedefs from being documented more than once. * * @ignore * @typedef {import("./types.js").weekDay} weekDay * Imports the 'weekDay' type from the "types.js" module * * @ignore * @typedef {import("./types.js").timeInit} timeInit * Imports the 'timeInit' type from the "types.js" module */ /** * @classdesc * iCalendar Time representation (similar to JS Date object). Fully * independent of system (OS) timezone / time. Unlike JS Date, the month * January is 1, not zero. * * @example * var time = new ICAL.Time({ * year: 2012, * month: 10, * day: 11 * minute: 0, * second: 0, * isDate: false * }); * * * @memberof ICAL */ class Time { static _dowCache = {}; static _wnCache = {}; /** * Returns the days in the given month * * @param {Number} month The month to check * @param {Number} year The year to check * @return {Number} The number of days in the month */ static daysInMonth(month, year) { let _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let days = 30; if (month < 1 || month > 12) return days; days = _daysInMonth[month]; if (month == 2) { days += Time.isLeapYear(year); } return days; } /** * Checks if the year is a leap year * * @param {Number} year The year to check * @return {Boolean} True, if the year is a leap year */ static isLeapYear(year) { if (year <= 1752) { return ((year % 4) == 0); } else { return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)); } } /** * Create a new ICAL.Time from the day of year and year. The date is returned * in floating timezone. * * @param {Number} aDayOfYear The day of year * @param {Number} aYear The year to create the instance in * @return {Time} The created instance with the calculated date */ static fromDayOfYear(aDayOfYear, aYear) { let year = aYear; let doy = aDayOfYear; let tt = new Time(); tt.auto_normalize = false; let is_leap = (Time.isLeapYear(year) ? 1 : 0); if (doy < 1) { year--; is_leap = (Time.isLeapYear(year) ? 1 : 0); doy += Time.daysInYearPassedMonth[is_leap][12]; return Time.fromDayOfYear(doy, year); } else if (doy > Time.daysInYearPassedMonth[is_leap][12]) { is_leap = (Time.isLeapYear(year) ? 1 : 0); doy -= Time.daysInYearPassedMonth[is_leap][12]; year++; return Time.fromDayOfYear(doy, year); } tt.year = year; tt.isDate = true; for (let month = 11; month >= 0; month--) { if (doy > Time.daysInYearPassedMonth[is_leap][month]) { tt.month = month + 1; tt.day = doy - Time.daysInYearPassedMonth[is_leap][month]; break; } } tt.auto_normalize = true; return tt; } /** * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. * * @deprecated Use {@link ICAL.Time.fromDateString} instead * @param {String} str The string to create from * @return {Time} The date/time instance */ static fromStringv2(str) { return new Time({ year: parseInt(str.slice(0, 4), 10), month: parseInt(str.slice(5, 7), 10), day: parseInt(str.slice(8, 10), 10), isDate: true }); } /** * Returns a new ICAL.Time instance from a date string, e.g 2015-01-02. * * @param {String} aValue The string to create from * @return {Time} The date/time instance */ static fromDateString(aValue) { // Dates should have no timezone. // Google likes to sometimes specify Z on dates // we specifically ignore that to avoid issues. // YYYY-MM-DD // 2012-10-10 return new Time({ year: strictParseInt(aValue.slice(0, 4)), month: strictParseInt(aValue.slice(5, 7)), day: strictParseInt(aValue.slice(8, 10)), isDate: true }); } /** * Returns a new ICAL.Time instance from a date-time string, e.g * 2015-01-02T03:04:05. If a property is specified, the timezone is set up * from the property's TZID parameter. * * @param {String} aValue The string to create from * @param {Property=} prop The property the date belongs to * @return {Time} The date/time instance */ static fromDateTimeString(aValue, prop) { if (aValue.length < 19) { throw new Error( 'invalid date-time value: "' + aValue + '"' ); } let zone; let zoneId; if (aValue.slice(-1) === 'Z') { zone = Timezone.utcTimezone; } else if (prop) { zoneId = prop.getParameter('tzid'); if (prop.parent) { if (prop.parent.name === 'standard' || prop.parent.name === 'daylight') { // Per RFC 5545 3.8.2.4 and 3.8.2.2, start/end date-times within // these components MUST be specified in local time. zone = Timezone.localTimezone; } else if (zoneId) { // If the desired time zone is defined within the component tree, // fetch its definition and prefer that. zone = prop.parent.getTimeZoneByID(zoneId); } } } const timeData = { year: strictParseInt(aValue.slice(0, 4)), month: strictParseInt(aValue.slice(5, 7)), day: strictParseInt(aValue.slice(8, 10)), hour: strictParseInt(aValue.slice(11, 13)), minute: strictParseInt(aValue.slice(14, 16)), second: strictParseInt(aValue.slice(17, 19)), }; // Although RFC 5545 requires that all TZIDs used within a file have a // corresponding time zone definition, we may not be parsing the full file // or we may be dealing with a non-compliant file; in either case, we can // check our own time zone service for the TZID in a last-ditch effort. if (zoneId && !zone) { timeData.timezone = zoneId; } // 2012-10-10T10:10:10(Z)? return new Time(timeData, zone); } /** * Returns a new ICAL.Time instance from a date or date-time string, * * @param {String} aValue The string to create from * @param {Property=} prop The property the date belongs to * @return {Time} The date/time instance */ static fromString(aValue, aProperty) { if (aValue.length > 10) { return Time.fromDateTimeString(aValue, aProperty); } else { return Time.fromDateString(aValue); } } /** * Creates a new ICAL.Time instance from the given Javascript Date. * * @param {?Date} aDate The Javascript Date to read, or null to reset * @param {Boolean} [useUTC=false] If true, the UTC values of the date will be used */ static fromJSDate(aDate, useUTC) { let tt = new Time(); return tt.fromJSDate(aDate, useUTC); } /** * Creates a new ICAL.Time instance from the the passed data object. * * @param {timeInit} aData Time initialization * @param {Timezone=} aZone Timezone this position occurs in */ static fromData = function fromData(aData, aZone) { let t = new Time(); return t.fromData(aData, aZone); }; /** * Creates a new ICAL.Time instance from the current moment. * The instance is “floating” - has no timezone relation. * To create an instance considering the time zone, call * ICAL.Time.fromJSDate(new Date(), true) * @return {Time} */ static now() { return Time.fromJSDate(new Date(), false); } /** * Returns the date on which ISO week number 1 starts. * * @see Time#weekNumber * @param {Number} aYear The year to search in * @param {weekDay=} aWeekStart The week start weekday, used for calculation. * @return {Time} The date on which week number 1 starts */ static weekOneStarts(aYear, aWeekStart) { let t = Time.fromData({ year: aYear, month: 1, day: 1, isDate: true }); let dow = t.dayOfWeek(); let wkst = aWeekStart || Time.DEFAULT_WEEK_START; if (dow > Time.THURSDAY) { t.day += 7; } if (wkst > Time.THURSDAY) { t.day -= 7; } t.day -= dow - wkst; return t; } /** * Get the dominical letter for the given year. Letters range from A - G for * common years, and AG to GF for leap years. * * @param {Number} yr The year to retrieve the letter for * @return {String} The dominical letter. */ static getDominicalLetter(yr) { let LTRS = "GFEDCBA"; let dom = (yr + (yr / 4 | 0) + (yr / 400 | 0) - (yr / 100 | 0) - 1) % 7; let isLeap = Time.isLeapYear(yr); if (isLeap) { return LTRS[(dom + 6) % 7] + LTRS[dom]; } else { return LTRS[dom]; } } static #epochTime = null; /** * January 1st, 1970 as an ICAL.Time. * @type {Time} * @constant * @instance */ static get epochTime() { if (!this.#epochTime) { this.#epochTime = Time.fromData({ year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, isDate: false, timezone: "Z" }); } return this.#epochTime; } static _cmp_attr(a, b, attr) { if (a[attr] > b[attr]) return 1; if (a[attr] < b[attr]) return -1; return 0; } /** * The days that have passed in the year after a given month. The array has * two members, one being an array of passed days for non-leap years, the * other analog for leap years. * @example * var isLeapYear = ICAL.Time.isLeapYear(year); * var passedDays = ICAL.Time.daysInYearPassedMonth[isLeapYear][month]; * @type {Array.>} */ static daysInYearPassedMonth = [ [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365], [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366] ]; static SUNDAY = 1; static MONDAY = 2; static TUESDAY = 3; static WEDNESDAY = 4; static THURSDAY = 5; static FRIDAY = 6; static SATURDAY = 7; /** * The default weekday for the WKST part. * @constant * @default ICAL.Time.MONDAY */ static DEFAULT_WEEK_START = 2; // MONDAY /** * Creates a new ICAL.Time instance. * * @param {timeInit} data Time initialization * @param {Timezone} zone timezone this position occurs in */ constructor(data, zone) { this.wrappedJSObject = this; /** * @type {timeInit} * @private */ this._time = Object.create(null); /* time defaults */ this._time.year = 0; this._time.month = 1; this._time.day = 1; this._time.hour = 0; this._time.minute = 0; this._time.second = 0; this._time.isDate = false; this.fromData(data, zone); } /** * The class identifier. * @constant * @type {String} * @default "icaltime" */ icalclass = "icaltime"; _cachedUnixTime = null; /** * The type name, to be used in the jCal object. This value may change and * is strictly defined by the {@link ICAL.Time#isDate isDate} member. * @type {String} * @default "date-time" */ get icaltype() { return this.isDate ? 'date' : 'date-time'; } /** * The timezone for this time. * @type {Timezone} */ zone = null; /** * Internal uses to indicate that a change has been made and the next read * operation must attempt to normalize the value (for example changing the * day to 33). * * @type {Boolean} * @private */ _pendingNormalization = false; /** * The year of this date. * @type {Number} */ get year() { return this._getTimeAttr('year'); } set year(val) { this._setTimeAttr('year', val); } /** * The month of this date. * @type {Number} */ get month() { return this._getTimeAttr('month'); } set month(val) { this._setTimeAttr('month', val); } /** * The day of this date. * @type {Number} */ get day() { return this._getTimeAttr('day'); } set day(val) { this._setTimeAttr('day', val); } /** * The hour of this date-time. * @type {Number} */ get hour() { return this._getTimeAttr('hour'); } set hour(val) { this._setTimeAttr('hour', val); } /** * The minute of this date-time. * @type {Number} */ get minute() { return this._getTimeAttr('minute'); } set minute(val) { this._setTimeAttr('minute', val); } /** * The second of this date-time. * @type {Number} */ get second() { return this._getTimeAttr('second'); } set second(val) { this._setTimeAttr('second', val); } /** * If true, the instance represents a date (as opposed to a date-time) * @type {Boolean} */ get isDate() { return this._getTimeAttr('isDate'); } set isDate(val) { this._setTimeAttr('isDate', val); } /** * @private * @param {String} attr Attribute to get (one of: year, month, * day, hour, minute, second, isDate) * @return {Number|Boolean} Current value for the attribute */ _getTimeAttr(attr) { if (this._pendingNormalization) { this._normalize(); this._pendingNormalization = false; } return this._time[attr]; } /** * @private * @param {String} attr Attribute to set (one of: year, month, * day, hour, minute, second, isDate) * @param {Number|Boolean} val New value for the attribute */ _setTimeAttr(attr, val) { // Check if isDate will be set and if was not set to normalize date. // This avoids losing days when seconds, minutes and hours are zeroed // what normalize will do when time is a date. if (attr === "isDate" && val && !this._time.isDate) { this.adjust(0, 0, 0, 0); } this._cachedUnixTime = null; this._pendingNormalization = true; this._time[attr] = val; } /** * Returns a clone of the time object. * * @return {Time} The cloned object */ clone() { return new Time(this._time, this.zone); } /** * Reset the time instance to epoch time */ reset() { this.fromData(Time.epochTime); this.zone = Timezone.utcTimezone; } /** * Reset the time instance to the given date/time values. * * @param {Number} year The year to set * @param {Number} month The month to set * @param {Number} day The day to set * @param {Number} hour The hour to set * @param {Number} minute The minute to set * @param {Number} second The second to set * @param {Timezone} timezone The timezone to set */ resetTo(year, month, day, hour, minute, second, timezone) { this.fromData({ year: year, month: month, day: day, hour: hour, minute: minute, second: second, zone: timezone }); } /** * Set up the current instance from the Javascript date value. * * @param {?Date} aDate The Javascript Date to read, or null to reset * @param {Boolean} [useUTC=false] If true, the UTC values of the date will be used */ fromJSDate(aDate, useUTC) { if (!aDate) { this.reset(); } else { if (useUTC) { this.zone = Timezone.utcTimezone; this.year = aDate.getUTCFullYear(); this.month = aDate.getUTCMonth() + 1; this.day = aDate.getUTCDate(); this.hour = aDate.getUTCHours(); this.minute = aDate.getUTCMinutes(); this.second = aDate.getUTCSeconds(); } else { this.zone = Timezone.localTimezone; this.year = aDate.getFullYear(); this.month = aDate.getMonth() + 1; this.day = aDate.getDate(); this.hour = aDate.getHours(); this.minute = aDate.getMinutes(); this.second = aDate.getSeconds(); } } this._cachedUnixTime = null; return this; } /** * Sets up the current instance using members from the passed data object. * * @param {timeInit} aData Time initialization * @param {Timezone=} aZone Timezone this position occurs in */ fromData(aData, aZone) { if (aData) { for (let [key, value] of Object.entries(aData)) { // ical type cannot be set if (key === 'icaltype') continue; this[key] = value; } } if (aZone) { this.zone = aZone; } if (aData && !("isDate" in aData)) { this.isDate = !("hour" in aData); } else if (aData && ("isDate" in aData)) { this.isDate = aData.isDate; } if (aData && "timezone" in aData) { let zone = TimezoneService.get( aData.timezone ); this.zone = zone || Timezone.localTimezone; } if (aData && "zone" in aData) { this.zone = aData.zone; } if (!this.zone) { this.zone = Timezone.localTimezone; } this._cachedUnixTime = null; return this; } /** * Calculate the day of week. * @param {weekDay=} aWeekStart * The week start weekday, defaults to SUNDAY * @return {weekDay} */ dayOfWeek(aWeekStart) { let firstDow = aWeekStart || Time.SUNDAY; let dowCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + firstDow; if (dowCacheKey in Time._dowCache) { return Time._dowCache[dowCacheKey]; } // Using Zeller's algorithm let q = this.day; let m = this.month + (this.month < 3 ? 12 : 0); let Y = this.year - (this.month < 3 ? 1 : 0); let h = (q + Y + trunc(((m + 1) * 26) / 10) + trunc(Y / 4)); if (true /* gregorian */) { // eslint-disable-line no-constant-condition h += trunc(Y / 100) * 6 + trunc(Y / 400); } else { /* c8 ignore next 2 */ h += 5; } // Normalize to 1 = wkst h = ((h + 7 - firstDow) % 7) + 1; Time._dowCache[dowCacheKey] = h; return h; } /** * Calculate the day of year. * @return {Number} */ dayOfYear() { let is_leap = (Time.isLeapYear(this.year) ? 1 : 0); let diypm = Time.daysInYearPassedMonth; return diypm[is_leap][this.month - 1] + this.day; } /** * Returns a copy of the current date/time, rewound to the start of the * week. The resulting ICAL.Time instance is of icaltype date, even if this * is a date-time. * * @param {weekDay=} aWeekStart * The week start weekday, defaults to SUNDAY * @return {Time} The start of the week (cloned) */ startOfWeek(aWeekStart) { let firstDow = aWeekStart || Time.SUNDAY; let result = this.clone(); result.day -= ((this.dayOfWeek() + 7 - firstDow) % 7); result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; } /** * Returns a copy of the current date/time, shifted to the end of the week. * The resulting ICAL.Time instance is of icaltype date, even if this is a * date-time. * * @param {weekDay=} aWeekStart * The week start weekday, defaults to SUNDAY * @return {Time} The end of the week (cloned) */ endOfWeek(aWeekStart) { let firstDow = aWeekStart || Time.SUNDAY; let result = this.clone(); result.day += (7 - this.dayOfWeek() + firstDow - Time.SUNDAY) % 7; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; } /** * Returns a copy of the current date/time, rewound to the start of the * month. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {Time} The start of the month (cloned) */ startOfMonth() { let result = this.clone(); result.day = 1; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; } /** * Returns a copy of the current date/time, shifted to the end of the * month. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {Time} The end of the month (cloned) */ endOfMonth() { let result = this.clone(); result.day = Time.daysInMonth(result.month, result.year); result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; } /** * Returns a copy of the current date/time, rewound to the start of the * year. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {Time} The start of the year (cloned) */ startOfYear() { let result = this.clone(); result.day = 1; result.month = 1; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; } /** * Returns a copy of the current date/time, shifted to the end of the * year. The resulting ICAL.Time instance is of icaltype date, even if * this is a date-time. * * @return {Time} The end of the year (cloned) */ endOfYear() { let result = this.clone(); result.day = 31; result.month = 12; result.isDate = true; result.hour = 0; result.minute = 0; result.second = 0; return result; } /** * First calculates the start of the week, then returns the day of year for * this date. If the day falls into the previous year, the day is zero or negative. * * @param {weekDay=} aFirstDayOfWeek * The week start weekday, defaults to SUNDAY * @return {Number} The calculated day of year */ startDoyWeek(aFirstDayOfWeek) { let firstDow = aFirstDayOfWeek || Time.SUNDAY; let delta = this.dayOfWeek() - firstDow; if (delta < 0) delta += 7; return this.dayOfYear() - delta; } /** * Get the dominical letter for the current year. Letters range from A - G * for common years, and AG to GF for leap years. * * @param {Number} yr The year to retrieve the letter for * @return {String} The dominical letter. */ getDominicalLetter() { return Time.getDominicalLetter(this.year); } /** * Finds the nthWeekDay relative to the current month (not day). The * returned value is a day relative the month that this month belongs to so * 1 would indicate the first of the month and 40 would indicate a day in * the following month. * * @param {Number} aDayOfWeek Day of the week see the day name constants * @param {Number} aPos Nth occurrence of a given week day values * of 1 and 0 both indicate the first weekday of that type. aPos may * be either positive or negative * * @return {Number} numeric value indicating a day relative * to the current month of this time object */ nthWeekDay(aDayOfWeek, aPos) { let daysInMonth = Time.daysInMonth(this.month, this.year); let weekday; let pos = aPos; let start = 0; let otherDay = this.clone(); if (pos >= 0) { otherDay.day = 1; // because 0 means no position has been given // 1 and 0 indicate the same day. if (pos != 0) { // remove the extra numeric value pos--; } // set current start offset to current day. start = otherDay.day; // find the current day of week let startDow = otherDay.dayOfWeek(); // calculate the difference between current // day of the week and desired day of the week let offset = aDayOfWeek - startDow; // if the offset goes into the past // week we add 7 so it goes into the next // week. We only want to go forward in time here. if (offset < 0) // this is really important otherwise we would // end up with dates from in the past. offset += 7; // add offset to start so start is the same // day of the week as the desired day of week. start += offset; // because we are going to add (and multiply) // the numeric value of the day we subtract it // from the start position so not to add it twice. start -= aDayOfWeek; // set week day weekday = aDayOfWeek; } else { // then we set it to the last day in the current month otherDay.day = daysInMonth; // find the ends weekday let endDow = otherDay.dayOfWeek(); pos++; weekday = (endDow - aDayOfWeek); if (weekday < 0) { weekday += 7; } weekday = daysInMonth - weekday; } weekday += pos * 7; return start + weekday; } /** * Checks if current time is the nth weekday, relative to the current * month. Will always return false when rule resolves outside of current * month. * * @param {weekDay} aDayOfWeek Day of week to check * @param {Number} aPos Relative position * @return {Boolean} True, if it is the nth weekday */ isNthWeekDay(aDayOfWeek, aPos) { let dow = this.dayOfWeek(); if (aPos === 0 && dow === aDayOfWeek) { return true; } // get pos let day = this.nthWeekDay(aDayOfWeek, aPos); if (day === this.day) { return true; } return false; } /** * Calculates the ISO 8601 week number. The first week of a year is the * week that contains the first Thursday. The year can have 53 weeks, if * January 1st is a Friday. * * Note there are regions where the first week of the year is the one that * starts on January 1st, which may offset the week number. Also, if a * different week start is specified, this will also affect the week * number. * * @see Time.weekOneStarts * @param {weekDay} aWeekStart The weekday the week starts with * @return {Number} The ISO week number */ weekNumber(aWeekStart) { let wnCacheKey = (this.year << 12) + (this.month << 8) + (this.day << 3) + aWeekStart; if (wnCacheKey in Time._wnCache) { return Time._wnCache[wnCacheKey]; } // This function courtesty of Julian Bucknall, published under the MIT license // http://www.boyet.com/articles/publishedarticles/calculatingtheisoweeknumb.html // plus some fixes to be able to use different week starts. let week1; let dt = this.clone(); dt.isDate = true; let isoyear = this.year; if (dt.month == 12 && dt.day > 25) { week1 = Time.weekOneStarts(isoyear + 1, aWeekStart); if (dt.compare(week1) < 0) { week1 = Time.weekOneStarts(isoyear, aWeekStart); } else { isoyear++; } } else { week1 = Time.weekOneStarts(isoyear, aWeekStart); if (dt.compare(week1) < 0) { week1 = Time.weekOneStarts(--isoyear, aWeekStart); } } let daysBetween = (dt.subtractDate(week1).toSeconds() / 86400); let answer = trunc(daysBetween / 7) + 1; Time._wnCache[wnCacheKey] = answer; return answer; } /** * Adds the duration to the current time. The instance is modified in * place. * * @param {Duration} aDuration The duration to add */ addDuration(aDuration) { let mult = (aDuration.isNegative ? -1 : 1); // because of the duration optimizations it is much // more efficient to grab all the values up front // then set them directly (which will avoid a normalization call). // So we don't actually normalize until we need it. let second = this.second; let minute = this.minute; let hour = this.hour; let day = this.day; second += mult * aDuration.seconds; minute += mult * aDuration.minutes; hour += mult * aDuration.hours; day += mult * aDuration.days; day += mult * 7 * aDuration.weeks; this.second = second; this.minute = minute; this.hour = hour; this.day = day; this._cachedUnixTime = null; } /** * Subtract the date details (_excluding_ timezone). Useful for finding * the relative difference between two time objects excluding their * timezone differences. * * @param {Time} aDate The date to subtract * @return {Duration} The difference as a duration */ subtractDate(aDate) { let unixTime = this.toUnixTime() + this.utcOffset(); let other = aDate.toUnixTime() + aDate.utcOffset(); return Duration.fromSeconds(unixTime - other); } /** * Subtract the date details, taking timezones into account. * * @param {Time} aDate The date to subtract * @return {Duration} The difference in duration */ subtractDateTz(aDate) { let unixTime = this.toUnixTime(); let other = aDate.toUnixTime(); return Duration.fromSeconds(unixTime - other); } /** * Compares the ICAL.Time instance with another one, or a period. * * @param {Time|Period} aOther The instance to compare with * @return {Number} -1, 0 or 1 for less/equal/greater */ compare(other) { if (other instanceof Period) { return -1 * other.compare(this); } else { let a = this.toUnixTime(); let b = other.toUnixTime(); if (a > b) return 1; if (b > a) return -1; return 0; } } /** * Compares only the date part of this instance with another one. * * @param {Time} other The instance to compare with * @param {Timezone} tz The timezone to compare in * @return {Number} -1, 0 or 1 for less/equal/greater */ compareDateOnlyTz(other, tz) { let a = this.convertToZone(tz); let b = other.convertToZone(tz); let rc = 0; if ((rc = Time._cmp_attr(a, b, "year")) != 0) return rc; if ((rc = Time._cmp_attr(a, b, "month")) != 0) return rc; if ((rc = Time._cmp_attr(a, b, "day")) != 0) return rc; return rc; } /** * Convert the instance into another timezone. The returned ICAL.Time * instance is always a copy. * * @param {Timezone} zone The zone to convert to * @return {Time} The copy, converted to the zone */ convertToZone(zone) { let copy = this.clone(); let zone_equals = (this.zone.tzid == zone.tzid); if (!this.isDate && !zone_equals) { Timezone.convert_time(copy, this.zone, zone); } copy.zone = zone; return copy; } /** * Calculates the UTC offset of the current date/time in the timezone it is * in. * * @return {Number} UTC offset in seconds */ utcOffset() { if (this.zone == Timezone.localTimezone || this.zone == Timezone.utcTimezone) { return 0; } else { return this.zone.utcOffset(this); } } /** * Returns an RFC 5545 compliant ical representation of this object. * * @return {String} ical date/date-time */ toICALString() { let string = this.toString(); if (string.length > 10) { return design.icalendar.value['date-time'].toICAL(string); } else { return design.icalendar.value.date.toICAL(string); } } /** * The string representation of this date/time, in jCal form * (including : and - separators). * @return {String} */ toString() { let result = this.year + '-' + pad2(this.month) + '-' + pad2(this.day); if (!this.isDate) { result += 'T' + pad2(this.hour) + ':' + pad2(this.minute) + ':' + pad2(this.second); if (this.zone === Timezone.utcTimezone) { result += 'Z'; } } return result; } /** * Converts the current instance to a Javascript date * @return {Date} */ toJSDate() { if (this.zone == Timezone.localTimezone) { if (this.isDate) { return new Date(this.year, this.month - 1, this.day); } else { return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second, 0); } } else { return new Date(this.toUnixTime() * 1000); } } _normalize() { if (this._time.isDate) { this._time.hour = 0; this._time.minute = 0; this._time.second = 0; } this.adjust(0, 0, 0, 0); return this; } /** * Adjust the date/time by the given offset * * @param {Number} aExtraDays The extra amount of days * @param {Number} aExtraHours The extra amount of hours * @param {Number} aExtraMinutes The extra amount of minutes * @param {Number} aExtraSeconds The extra amount of seconds * @param {Number=} aTime The time to adjust, defaults to the * current instance. */ adjust(aExtraDays, aExtraHours, aExtraMinutes, aExtraSeconds, aTime) { let minutesOverflow, hoursOverflow, daysOverflow = 0, yearsOverflow = 0; let second, minute, hour, day; let daysInMonth; let time = aTime || this._time; if (!time.isDate) { second = time.second + aExtraSeconds; time.second = second % 60; minutesOverflow = trunc(second / 60); if (time.second < 0) { time.second += 60; minutesOverflow--; } minute = time.minute + aExtraMinutes + minutesOverflow; time.minute = minute % 60; hoursOverflow = trunc(minute / 60); if (time.minute < 0) { time.minute += 60; hoursOverflow--; } hour = time.hour + aExtraHours + hoursOverflow; time.hour = hour % 24; daysOverflow = trunc(hour / 24); if (time.hour < 0) { time.hour += 24; daysOverflow--; } } // Adjust month and year first, because we need to know what month the day // is in before adjusting it. if (time.month > 12) { yearsOverflow = trunc((time.month - 1) / 12); } else if (time.month < 1) { yearsOverflow = trunc(time.month / 12) - 1; } time.year += yearsOverflow; time.month -= 12 * yearsOverflow; // Now take care of the days (and adjust month if needed) day = time.day + aExtraDays + daysOverflow; if (day > 0) { for (;;) { daysInMonth = Time.daysInMonth(time.month, time.year); if (day <= daysInMonth) { break; } time.month++; if (time.month > 12) { time.year++; time.month = 1; } day -= daysInMonth; } } else { while (day <= 0) { if (time.month == 1) { time.year--; time.month = 12; } else { time.month--; } day += Time.daysInMonth(time.month, time.year); } } time.day = day; this._cachedUnixTime = null; return this; } /** * Sets up the current instance from unix time, the number of seconds since * January 1st, 1970. * * @param {Number} seconds The seconds to set up with */ fromUnixTime(seconds) { this.zone = Timezone.utcTimezone; // We could use `fromJSDate` here, but this is about twice as fast. // We could also clone `epochTime` and use `adjust` for a more // ical.js-centric approach, but this is about 100 times as fast. let date = new Date(seconds * 1000); this.year = date.getUTCFullYear(); this.month = date.getUTCMonth() + 1; this.day = date.getUTCDate(); if (this._time.isDate) { this.hour = 0; this.minute = 0; this.second = 0; } else { this.hour = date.getUTCHours(); this.minute = date.getUTCMinutes(); this.second = date.getUTCSeconds(); } this._cachedUnixTime = null; } /** * Converts the current instance to seconds since January 1st 1970. * * @return {Number} Seconds since 1970 */ toUnixTime() { if (this._cachedUnixTime !== null) { return this._cachedUnixTime; } let offset = this.utcOffset(); // we use the offset trick to ensure // that we are getting the actual UTC time let ms = Date.UTC( this.year, this.month - 1, this.day, this.hour, this.minute, this.second - offset ); // seconds this._cachedUnixTime = ms / 1000; return this._cachedUnixTime; } /** * Converts time to into Object which can be serialized then re-created * using the constructor. * * @example * // toJSON will automatically be called * var json = JSON.stringify(mytime); * * var deserialized = JSON.parse(json); * * var time = new ICAL.Time(deserialized); * * @return {Object} */ toJSON() { let copy = [ 'year', 'month', 'day', 'hour', 'minute', 'second', 'isDate' ]; let result = Object.create(null); let i = 0; let len = copy.length; let prop; for (; i < len; i++) { prop = copy[i]; result[prop] = this[prop]; } if (this.zone) { result.timezone = this.zone.tzid; } return result; } } export default Time;