375 lines
9.3 KiB
JavaScript
375 lines
9.3 KiB
JavaScript
/* 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 { isStrictlyNaN, trunc } from "./helpers.js";
|
|
|
|
const DURATION_LETTERS = /([PDWHMTS]{1,1})/;
|
|
const DATA_PROPS_TO_COPY = ["weeks", "days", "hours", "minutes", "seconds", "isNegative"];
|
|
|
|
/**
|
|
* This class represents the "duration" value type, with various calculation
|
|
* and manipulation methods.
|
|
*
|
|
* @memberof ICAL
|
|
*/
|
|
class Duration {
|
|
/**
|
|
* Returns a new ICAL.Duration instance from the passed seconds value.
|
|
*
|
|
* @param {Number} aSeconds The seconds to create the instance from
|
|
* @return {Duration} The newly created duration instance
|
|
*/
|
|
static fromSeconds(aSeconds) {
|
|
return (new Duration()).fromSeconds(aSeconds);
|
|
}
|
|
|
|
/**
|
|
* Checks if the given string is an iCalendar duration value.
|
|
*
|
|
* @param {String} value The raw ical value
|
|
* @return {Boolean} True, if the given value is of the
|
|
* duration ical type
|
|
*/
|
|
static isValueString(string) {
|
|
return (string[0] === 'P' || string[1] === 'P');
|
|
}
|
|
|
|
/**
|
|
* Creates a new {@link ICAL.Duration} instance from the passed string.
|
|
*
|
|
* @param {String} aStr The string to parse
|
|
* @return {Duration} The created duration instance
|
|
*/
|
|
static fromString(aStr) {
|
|
let pos = 0;
|
|
let dict = Object.create(null);
|
|
let chunks = 0;
|
|
|
|
while ((pos = aStr.search(DURATION_LETTERS)) !== -1) {
|
|
let type = aStr[pos];
|
|
let numeric = aStr.slice(0, Math.max(0, pos));
|
|
aStr = aStr.slice(pos + 1);
|
|
|
|
chunks += parseDurationChunk(type, numeric, dict);
|
|
}
|
|
|
|
if (chunks < 2) {
|
|
// There must be at least a chunk with "P" and some unit chunk
|
|
throw new Error(
|
|
'invalid duration value: Not enough duration components in "' + aStr + '"'
|
|
);
|
|
}
|
|
|
|
return new Duration(dict);
|
|
}
|
|
|
|
/**
|
|
* Creates a new ICAL.Duration instance from the given data object.
|
|
*
|
|
* @param {Object} aData An object with members of the duration
|
|
* @param {Number=} aData.weeks Duration in weeks
|
|
* @param {Number=} aData.days Duration in days
|
|
* @param {Number=} aData.hours Duration in hours
|
|
* @param {Number=} aData.minutes Duration in minutes
|
|
* @param {Number=} aData.seconds Duration in seconds
|
|
* @param {Boolean=} aData.isNegative If true, the duration is negative
|
|
* @return {Duration} The createad duration instance
|
|
*/
|
|
static fromData(aData) {
|
|
return new Duration(aData);
|
|
}
|
|
|
|
/**
|
|
* Creates a new ICAL.Duration instance.
|
|
*
|
|
* @param {Object} data An object with members of the duration
|
|
* @param {Number=} data.weeks Duration in weeks
|
|
* @param {Number=} data.days Duration in days
|
|
* @param {Number=} data.hours Duration in hours
|
|
* @param {Number=} data.minutes Duration in minutes
|
|
* @param {Number=} data.seconds Duration in seconds
|
|
* @param {Boolean=} data.isNegative If true, the duration is negative
|
|
*/
|
|
constructor(data) {
|
|
this.wrappedJSObject = this;
|
|
this.fromData(data);
|
|
}
|
|
|
|
/**
|
|
* The weeks in this duration
|
|
* @type {Number}
|
|
* @default 0
|
|
*/
|
|
weeks = 0;
|
|
|
|
/**
|
|
* The days in this duration
|
|
* @type {Number}
|
|
* @default 0
|
|
*/
|
|
days = 0;
|
|
|
|
/**
|
|
* The days in this duration
|
|
* @type {Number}
|
|
* @default 0
|
|
*/
|
|
hours = 0;
|
|
|
|
/**
|
|
* The minutes in this duration
|
|
* @type {Number}
|
|
* @default 0
|
|
*/
|
|
minutes = 0;
|
|
|
|
/**
|
|
* The seconds in this duration
|
|
* @type {Number}
|
|
* @default 0
|
|
*/
|
|
seconds = 0;
|
|
|
|
/**
|
|
* The seconds in this duration
|
|
* @type {Boolean}
|
|
* @default false
|
|
*/
|
|
isNegative = false;
|
|
|
|
/**
|
|
* The class identifier.
|
|
* @constant
|
|
* @type {String}
|
|
* @default "icalduration"
|
|
*/
|
|
icalclass = "icalduration";
|
|
|
|
/**
|
|
* The type name, to be used in the jCal object.
|
|
* @constant
|
|
* @type {String}
|
|
* @default "duration"
|
|
*/
|
|
icaltype = "duration";
|
|
|
|
/**
|
|
* Returns a clone of the duration object.
|
|
*
|
|
* @return {Duration} The cloned object
|
|
*/
|
|
clone() {
|
|
return Duration.fromData(this);
|
|
}
|
|
|
|
/**
|
|
* The duration value expressed as a number of seconds.
|
|
*
|
|
* @return {Number} The duration value in seconds
|
|
*/
|
|
toSeconds() {
|
|
let seconds = this.seconds + 60 * this.minutes + 3600 * this.hours +
|
|
86400 * this.days + 7 * 86400 * this.weeks;
|
|
return (this.isNegative ? -seconds : seconds);
|
|
}
|
|
|
|
/**
|
|
* Reads the passed seconds value into this duration object. Afterwards,
|
|
* members like {@link ICAL.Duration#days days} and {@link ICAL.Duration#weeks weeks} will be set up
|
|
* accordingly.
|
|
*
|
|
* @param {Number} aSeconds The duration value in seconds
|
|
* @return {Duration} Returns this instance
|
|
*/
|
|
fromSeconds(aSeconds) {
|
|
let secs = Math.abs(aSeconds);
|
|
|
|
this.isNegative = (aSeconds < 0);
|
|
this.days = trunc(secs / 86400);
|
|
|
|
// If we have a flat number of weeks, use them.
|
|
if (this.days % 7 == 0) {
|
|
this.weeks = this.days / 7;
|
|
this.days = 0;
|
|
} else {
|
|
this.weeks = 0;
|
|
}
|
|
|
|
secs -= (this.days + 7 * this.weeks) * 86400;
|
|
|
|
this.hours = trunc(secs / 3600);
|
|
secs -= this.hours * 3600;
|
|
|
|
this.minutes = trunc(secs / 60);
|
|
secs -= this.minutes * 60;
|
|
|
|
this.seconds = secs;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets up the current instance using members from the passed data object.
|
|
*
|
|
* @param {Object} aData An object with members of the duration
|
|
* @param {Number=} aData.weeks Duration in weeks
|
|
* @param {Number=} aData.days Duration in days
|
|
* @param {Number=} aData.hours Duration in hours
|
|
* @param {Number=} aData.minutes Duration in minutes
|
|
* @param {Number=} aData.seconds Duration in seconds
|
|
* @param {Boolean=} aData.isNegative If true, the duration is negative
|
|
*/
|
|
fromData(aData) {
|
|
for (let prop of DATA_PROPS_TO_COPY) {
|
|
if (aData && prop in aData) {
|
|
this[prop] = aData[prop];
|
|
} else {
|
|
this[prop] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the duration instance to the default values, i.e. PT0S
|
|
*/
|
|
reset() {
|
|
this.isNegative = false;
|
|
this.weeks = 0;
|
|
this.days = 0;
|
|
this.hours = 0;
|
|
this.minutes = 0;
|
|
this.seconds = 0;
|
|
}
|
|
|
|
/**
|
|
* Compares the duration instance with another one.
|
|
*
|
|
* @param {Duration} aOther The instance to compare with
|
|
* @return {Number} -1, 0 or 1 for less/equal/greater
|
|
*/
|
|
compare(aOther) {
|
|
let thisSeconds = this.toSeconds();
|
|
let otherSeconds = aOther.toSeconds();
|
|
return (thisSeconds > otherSeconds) - (thisSeconds < otherSeconds);
|
|
}
|
|
|
|
/**
|
|
* Normalizes the duration instance. For example, a duration with a value
|
|
* of 61 seconds will be normalized to 1 minute and 1 second.
|
|
*/
|
|
normalize() {
|
|
this.fromSeconds(this.toSeconds());
|
|
}
|
|
|
|
/**
|
|
* The string representation of this duration.
|
|
* @return {String}
|
|
*/
|
|
toString() {
|
|
if (this.toSeconds() == 0) {
|
|
return "PT0S";
|
|
} else {
|
|
let str = "";
|
|
if (this.isNegative) str += "-";
|
|
str += "P";
|
|
let hasWeeks = false;
|
|
if (this.weeks) {
|
|
if (this.days || this.hours || this.minutes || this.seconds) {
|
|
str += (this.weeks * 7 + this.days) + "D";
|
|
} else {
|
|
str += (this.weeks + "W");
|
|
hasWeeks = true;
|
|
}
|
|
} else if (this.days) {
|
|
str += (this.days + "D");
|
|
}
|
|
|
|
if (!hasWeeks) {
|
|
if (this.hours || this.minutes || this.seconds) {
|
|
str += "T";
|
|
if (this.hours) {
|
|
str += this.hours + "H";
|
|
}
|
|
|
|
if (this.minutes) {
|
|
str += this.minutes + "M";
|
|
}
|
|
|
|
if (this.seconds) {
|
|
str += this.seconds + "S";
|
|
}
|
|
}
|
|
}
|
|
|
|
return str;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The iCalendar string representation of this duration.
|
|
* @return {String}
|
|
*/
|
|
toICALString() {
|
|
return this.toString();
|
|
}
|
|
}
|
|
export default Duration;
|
|
|
|
/**
|
|
* Internal helper function to handle a chunk of a duration.
|
|
*
|
|
* @private
|
|
* @param {String} letter type of duration chunk
|
|
* @param {String} number numeric value or -/+
|
|
* @param {Object} dict target to assign values to
|
|
*/
|
|
function parseDurationChunk(letter, number, object) {
|
|
let type;
|
|
switch (letter) {
|
|
case 'P':
|
|
if (number && number === '-') {
|
|
object.isNegative = true;
|
|
} else {
|
|
object.isNegative = false;
|
|
}
|
|
// period
|
|
break;
|
|
case 'D':
|
|
type = 'days';
|
|
break;
|
|
case 'W':
|
|
type = 'weeks';
|
|
break;
|
|
case 'H':
|
|
type = 'hours';
|
|
break;
|
|
case 'M':
|
|
type = 'minutes';
|
|
break;
|
|
case 'S':
|
|
type = 'seconds';
|
|
break;
|
|
default:
|
|
// Not a valid chunk
|
|
return 0;
|
|
}
|
|
|
|
if (type) {
|
|
if (!number && number !== 0) {
|
|
throw new Error(
|
|
'invalid duration value: Missing number before "' + letter + '"'
|
|
);
|
|
}
|
|
let num = parseInt(number, 10);
|
|
if (isStrictlyNaN(num)) {
|
|
throw new Error(
|
|
'invalid duration value: Invalid number "' + number + '" before "' + letter + '"'
|
|
);
|
|
}
|
|
object[type] = num;
|
|
}
|
|
|
|
return 1;
|
|
}
|