ical.js-one.com
Version:
[](http://travis-ci.org/mozilla-comm/ical.js)
430 lines (345 loc) • 12.5 kB
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, 2011-2012 */
"use strict";
(typeof(ICAL) === 'undefined')? ICAL = {} : '';
(function() {
var OPTIONS = ["tzid", "location", "tznames",
"latitude", "longitude"];
/**
* Timezone representation, created by passing in a tzid and component.
*
* var vcalendar;
* var timezoneComp = vcalendar.getFirstSubcomponent('vtimezone');
* var tzid = timezoneComp.getFirstPropertyValue('tzid');
*
* var timezone = new ICAL.Timezone({
* component: timezoneComp,
* tzid
* });
*
*
* @param {Object} data options for class (see above).
*/
ICAL.Timezone = function icaltimezone(data) {
this.wrappedJSObject = this;
this.fromData(data);
};
ICAL.Timezone.prototype = {
tzid: "",
location: "",
tznames: "",
latitude: 0.0,
longitude: 0.0,
component: null,
expandedUntilYear: 0,
icalclass: "icaltimezone",
fromData: function fromData(aData) {
this.expandedUntilYear = 0;
this.changes = [];
if (aData instanceof ICAL.Component) {
// Either a component is passed directly
this.component = aData;
} else {
// Otherwise the component may be in the data object
if (aData && "component" in aData) {
if (typeof aData.component == "string") {
// If a string was passed, parse it as a component
var icalendar = ICAL.parse(aData.component);
this.component = new ICAL.Component(icalendar[1]);
} else if (aData.component instanceof ICAL.Component) {
// If it was a component already, then just set it
this.component = aData.component;
} else {
// Otherwise just null out the component
this.component = null;
}
}
// Copy remaining passed properties
for (var key in OPTIONS) {
var prop = OPTIONS[key];
if (aData && prop in aData) {
this[prop] = aData[prop];
}
}
}
// If we have a component but no TZID, attempt to get it from the
// component's properties.
if (this.component instanceof ICAL.Component && !this.tzid) {
this.tzid = this.component.getFirstPropertyValue('tzid');
}
return this;
},
findChangeNumToUse: function findChangeNumToUse(tt) {
this._ensureCoverage(tt.year);
if (!this.changes.length) {
return 0;
}
var tt_change = {
year: tt.year,
month: tt.month,
day: tt.day,
hour: tt.hour,
minute: tt.minute,
second: tt.second
};
var change_num = this._findNearbyChange(tt_change);
var change_num_to_use = -1;
var step = 1;
// TODO: replace with bin search?
for (;;) {
var change = ICAL.helpers.clone(this.changes[change_num], true);
if (change.utcOffset < change.prevUtcOffset) {
ICAL.Timezone.adjust_change(change, 0, 0, 0, change.utcOffset);
} else {
ICAL.Timezone.adjust_change(change, 0, 0, 0,
change.prevUtcOffset);
}
var cmp = ICAL.Timezone._compare_change_fn(tt_change, change);
if (cmp >= 0) {
change_num_to_use = change_num;
} else {
step = -1;
}
if (step == -1 && change_num_to_use != -1) {
break;
}
change_num += step;
if (change_num < 0) {
return 0;
}
if (change_num >= this.changes.length) {
break;
}
}
return change_num_to_use;
},
findChange: function findChange(tt) {
return this.changes[this.findChangeNumToUse(tt)];
},
/**
* Finds the utcOffset the given time would occur in this timezone.
*
* @return {Number} utc offset in seconds.
*/
utcOffset: function utcOffset(tt) {
if (this == ICAL.Timezone.utcTimezone || this == ICAL.Timezone.localTimezone) {
return 0;
}
// This is duplicated, and it seems that tt could just be used in the below, but just to be safe:
var tt_change = {
year: tt.year,
month: tt.month,
day: tt.day,
hour: tt.hour,
minute: tt.minute,
second: tt.second
};
var change_num_to_use = this.findChangeNumToUse(tt),
zone_change = this.changes[change_num_to_use],
utcOffset_change = zone_change.utcOffset - zone_change.prevUtcOffset;
if (utcOffset_change < 0 && change_num_to_use > 0) {
var tmp_change = ICAL.helpers.clone(zone_change, true);
ICAL.Timezone.adjust_change(tmp_change, 0, 0, 0,
tmp_change.prevUtcOffset);
if (ICAL.Timezone._compare_change_fn(tt_change, tmp_change) < 0) {
var prev_zone_change = this.changes[change_num_to_use - 1];
var want_daylight = false; // TODO
if (zone_change.is_daylight != want_daylight &&
prev_zone_change.is_daylight == want_daylight) {
zone_change = prev_zone_change;
}
}
}
// TODO return is_daylight?
return zone_change.utcOffset;
},
_findNearbyChange: function icaltimezone_find_nearby_change(change) {
// find the closest match
var idx = ICAL.helpers.binsearchInsert(
this.changes,
change,
ICAL.Timezone._compare_change_fn
);
if (idx >= this.changes.length) {
return this.changes.length - 1;
}
return idx;
},
_ensureCoverage: function(aYear) {
if (ICAL.Timezone._minimumExpansionYear == -1) {
var today = ICAL.Time.now();
ICAL.Timezone._minimumExpansionYear = today.year;
}
var changesEndYear = aYear;
if (changesEndYear < ICAL.Timezone._minimumExpansionYear) {
changesEndYear = ICAL.Timezone._minimumExpansionYear;
}
changesEndYear += ICAL.Timezone.EXTRA_COVERAGE;
if (changesEndYear > ICAL.Timezone.MAX_YEAR) {
changesEndYear = ICAL.Timezone.MAX_YEAR;
}
if (!this.changes.length || this.expandedUntilYear < aYear) {
var subcomps = this.component.getAllSubcomponents();
var compLen = subcomps.length;
var compIdx = 0;
for (; compIdx < compLen; compIdx++) {
this._expandComponent(
subcomps[compIdx], changesEndYear, this.changes
);
}
this.changes.sort(ICAL.Timezone._compare_change_fn);
this.expandedUntilYear = changesEndYear;
}
},
_expandComponent: function(aComponent, aYear, changes) {
if (!aComponent.hasProperty("dtstart") ||
!aComponent.hasProperty("tzoffsetto") ||
!aComponent.hasProperty("tzoffsetfrom")) {
return null;
}
var dtstart = aComponent.getFirstProperty("dtstart").getFirstValue();
function convert_tzoffset(offset) {
return offset.factor * (offset.hours * 3600 + offset.minutes * 60);
}
function init_changes() {
var changebase = {};
changebase.is_daylight = (aComponent.name == "daylight");
changebase.utcOffset = convert_tzoffset(
aComponent.getFirstProperty("tzoffsetto").getFirstValue()
);
changebase.prevUtcOffset = convert_tzoffset(
aComponent.getFirstProperty("tzoffsetfrom").getFirstValue()
);
return changebase;
}
if (!aComponent.hasProperty("rrule") && !aComponent.hasProperty("rdate")) {
var change = init_changes();
change.year = dtstart.year;
change.month = dtstart.month;
change.day = dtstart.day;
change.hour = dtstart.hour;
change.minute = dtstart.minute;
change.second = dtstart.second;
change._originalComponent = aComponent;
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
changes.push(change);
} else {
var props = aComponent.getAllProperties("rdate");
for (var rdatekey in props) {
var rdate = props[rdatekey];
var time = rdate.getFirstValue();
var change = init_changes();
change.year = time.year;
change.month = time.month;
change.day = time.day;
if (time.isDate) {
change.hour = dtstart.hour;
change.minute = dtstart.minute;
change.second = dtstart.second;
if (dtstart.zone != ICAL.Timezone.utcTimezone) {
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
}
} else {
change.hour = time.hour;
change.minute = time.minute;
change.second = time.second;
if (time.zone != ICAL.Timezone.utcTimezone) {
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
}
}
change._originalComponent = aComponent;
changes.push(change);
}
var rrule = aComponent.getFirstProperty("rrule");
if (rrule) {
rrule = rrule.getFirstValue();
var change = init_changes();
if (rrule.until && rrule.until.zone == ICAL.Timezone.utcTimezone) {
rrule.until.adjust(0, 0, 0, change.prevUtcOffset);
rrule.until.zone = ICAL.Timezone.localTimezone;
}
var iterator = rrule.iterator(dtstart);
var occ;
while ((occ = iterator.next())) {
var change = init_changes();
if (occ.year > aYear || !occ) {
break;
}
change.year = occ.year;
change.month = occ.month;
change.day = occ.day;
change.hour = occ.hour;
change.minute = occ.minute;
change.second = occ.second;
change.isDate = occ.isDate;
change._originalComponent = aComponent;
ICAL.Timezone.adjust_change(change, 0, 0, 0,
-change.prevUtcOffset);
changes.push(change);
}
}
}
return changes;
},
toString: function toString() {
return (this.tznames ? this.tznames : this.tzid);
}
};
ICAL.Timezone._compare_change_fn = function icaltimezone_compare_change_fn(a, b) {
if (a.year < b.year) return -1;
else if (a.year > b.year) return 1;
if (a.month < b.month) return -1;
else if (a.month > b.month) return 1;
if (a.day < b.day) return -1;
else if (a.day > b.day) return 1;
if (a.hour < b.hour) return -1;
else if (a.hour > b.hour) return 1;
if (a.minute < b.minute) return -1;
else if (a.minute > b.minute) return 1;
if (a.second < b.second) return -1;
else if (a.second > b.second) return 1;
return 0;
};
ICAL.Timezone.convert_time = function icaltimezone_convert_time(tt, from_zone, to_zone) {
if (tt.isDate ||
from_zone.tzid == to_zone.tzid ||
from_zone == ICAL.Timezone.localTimezone ||
to_zone == ICAL.Timezone.localTimezone) {
tt.zone = to_zone;
return tt;
}
var utcOffset = from_zone.utcOffset(tt);
tt.adjust(0, 0, 0, - utcOffset);
utcOffset = to_zone.utcOffset(tt);
tt.adjust(0, 0, 0, utcOffset);
return null;
};
ICAL.Timezone.fromData = function icaltimezone_fromData(aData) {
var tt = new ICAL.Timezone();
return tt.fromData(aData);
};
ICAL.Timezone.utcTimezone = ICAL.Timezone.fromData({
tzid: "UTC"
});
ICAL.Timezone.localTimezone = ICAL.Timezone.fromData({
tzid: "floating"
});
ICAL.Timezone.adjust_change = function icaltimezone_adjust_change(change, days, hours, minutes, seconds) {
return ICAL.Time.prototype.adjust.call(
change,
days,
hours,
minutes,
seconds,
change
);
};
ICAL.Timezone._minimumExpansionYear = -1;
ICAL.Timezone.MAX_YEAR = 2035; // TODO this is because of time_t, which we don't need. Still usefull?
ICAL.Timezone.EXTRA_COVERAGE = 5;
})();