Comparing version 0.4.1 to 0.5.0
@@ -30,184 +30,8 @@ // Copyright (C) 2011 Tri Tech Computers Ltd. | ||
var types = require('./types'); | ||
var parser = require('./parser'); | ||
var CalendarObject = require('./base').CalendarObject; | ||
var CalendarProperty = require('./base').CalendarProperty; | ||
var schema = require('./base').schema; | ||
var format_value = exports.format_value = types.format_value; | ||
var parse_value = exports.parse_value = types.parse_value; | ||
var RRule = exports.RRule = require('./recur').RRule; | ||
// Maximum number of octets in a single iCalendar line | ||
var MAX_LINE = 75; | ||
exports.PRODID = '-//Tri Tech Computers//node-icalendar//EN'; | ||
exports.parse_calendar = parser.parse_calendar; | ||
var CalendarObject = exports.CalendarObject = function(calendar, element) { | ||
this.calendar = calendar; | ||
this.element = element; | ||
this.components = {}; | ||
this.properties = {}; | ||
} | ||
// Create an element of the correct type | ||
CalendarObject.create = function(element, calendar) { | ||
var factory = (schema[element] || {}).factory; | ||
return (factory !== undefined | ||
? new factory(calendar) | ||
: new CalendarObject(calendar, comp)); | ||
} | ||
// Recursively generates a clone of some calendar object | ||
CalendarObject.prototype.clone = function() { | ||
var obj = CalendarObject.create(this.element, this.calendar); | ||
for(var prop in this.properties) | ||
obj.addProperty(this.properties[prop]); | ||
var comp = this.getComponents(); | ||
for(var i=0; i<comp.length; ++i) | ||
obj.addComponent(comp[i]); | ||
return obj; | ||
} | ||
CalendarObject.prototype.addProperty = function(prop, value, parameters) { | ||
if(!(prop instanceof CalendarProperty)) | ||
prop = new CalendarProperty(prop, value, parameters); | ||
else | ||
prop = prop.clone(); | ||
// TODO: What about multiple occurances of the same property? | ||
this.properties[prop.name] = prop; | ||
return prop; | ||
} | ||
CalendarObject.prototype.addComponent = function(comp) { | ||
if(!(comp instanceof CalendarObject)) { | ||
var factory = (schema[comp] || {}).factory; | ||
comp = factory !== undefined | ||
? new factory(this.calendar) | ||
: new CalendarObject(this.calendar, comp); | ||
} | ||
// Create a copy of the component if it's from a different | ||
// calendar object to prevent changes from one place happening | ||
// somewhere else as well | ||
if(comp.calendar && comp.calendar !== this.calendar) | ||
comp = comp.clone(); | ||
this.components[comp.element] = this.components[comp.element] || []; | ||
this.components[comp.element].push(comp); | ||
comp.calendar = this.calendar; | ||
return comp; | ||
} | ||
CalendarObject.prototype.getComponents = function(type) { | ||
if(type === undefined) { | ||
var all = []; | ||
for(var c in this.components) | ||
all = all.concat(this.components[c]); | ||
return all; | ||
} | ||
return this.components[type] || []; | ||
} | ||
CalendarObject.prototype.getProperty = function(prop) { | ||
return this.properties[prop]; | ||
} | ||
CalendarObject.prototype.getPropertyValue = function(prop) { | ||
return (this.properties[prop] || {}).value; | ||
} | ||
CalendarObject.prototype.toString = function() { | ||
// Make sure output always includes a VCALENDAR object | ||
var output; | ||
if(this.element == 'VCALENDAR') | ||
output = this.format(); | ||
else { | ||
var ical = new iCalendar(); | ||
ical.addComponent(this); | ||
output = ical.format() | ||
} | ||
output.push(''); // <-- Add empty element to ensure trailing CRLF | ||
return output.join('\r\n'); | ||
} | ||
CalendarObject.prototype.format = function() { | ||
var lines = ['BEGIN:'+this.element]; | ||
for(var i in this.properties) | ||
lines.push.apply(lines, this.properties[i].format()); | ||
for(var comp in this.components) { | ||
var comp = this.components[comp]; | ||
for(var i=0; i < comp.length; ++i) | ||
lines.push.apply(lines, comp[i].format()); | ||
} | ||
lines.push('END:'+this.element); | ||
return lines; | ||
} | ||
var CalendarProperty = exports.CalendarProperty = function(name, value, parameters) { | ||
var propdef = properties[name]; | ||
this.type = propdef && propdef.type ? propdef.type : 'TEXT'; | ||
this.name = name; | ||
this.value = value; | ||
this.parameters = parameters || {}; | ||
} | ||
CalendarProperty.prototype.clone = function() { | ||
var obj = new CalendarProperty(this.name, this.value); | ||
obj.type = this.type; | ||
// TODO: Copy type and value instances in the case of objects, dates, arrays | ||
for(var param in this.parameters) | ||
obj.parameters[param] = this.parameters[param]; | ||
return obj; | ||
} | ||
CalendarProperty.prototype.getParameter = function(param) { | ||
return this.parameters[param]; | ||
} | ||
CalendarProperty.prototype.format = function() { | ||
var data = new Buffer(this.name+':'+format_value(this.type, this.value)); | ||
var pos = 0, len; | ||
var output = []; | ||
while(true) { | ||
len = MAX_LINE; | ||
if(pos+len >= data.length) | ||
len = data.length-pos; | ||
// We're in the middle of a unicode character if the high bit is set and | ||
// the next byte is 10xxxxxx (or 0x80). Don't split it in half. | ||
// Wind backward until we find the start character... | ||
while((data[pos+len] & 0xc0) == 0x80) | ||
len--; | ||
output.push(data.toString('utf8', pos, pos+len)); | ||
if(pos+len >= data.length) | ||
break; | ||
// Insert the space for the start of the next line... | ||
pos += len-1; | ||
data[pos] = 0x20; | ||
} | ||
return output; | ||
} | ||
var iCalendar = exports.iCalendar = function(empty) { | ||
@@ -218,3 +42,3 @@ CalendarObject.call(this, this, 'VCALENDAR'); | ||
if(!empty) { | ||
this.addProperty('PRODID', exports.PRODID); | ||
this.addProperty('PRODID', require('./index').PRODID); | ||
this.addProperty('VERSION', '2.0'); | ||
@@ -225,4 +49,2 @@ } | ||
iCalendar.parse = parser.parse_calendar; | ||
iCalendar.prototype.events = function() { return this.components['VEVENT'] || []; } | ||
@@ -239,163 +61,23 @@ | ||
var VEvent = exports.VEvent = function(calendar, uid) { | ||
if(!(calendar instanceof iCalendar)) { | ||
uid = calendar; | ||
calendar = null; | ||
} | ||
CalendarObject.call(this, calendar, 'VEVENT'); | ||
// TODO: Move validation to its own method | ||
// if(uid === undefined) | ||
// throw Error("UID is a required parameter"); | ||
if(uid !== undefined) { | ||
this.addProperty('DTSTAMP', new Date()); | ||
this.addProperty('UID', uid); | ||
} | ||
} | ||
util.inherits(VEvent, CalendarObject); | ||
VEvent.prototype.setSummary = function(summ) { | ||
this.addProperty('SUMMARY', summ); | ||
} | ||
VEvent.prototype.setDescription = function(desc) { | ||
this.addProperty('DESCRIPTION', desc); | ||
} | ||
VEvent.prototype.setDate = function(start, end) { | ||
this.addProperty('DTSTART', start); | ||
if(end instanceof Date) | ||
this.addProperty('DTEND', end); | ||
else | ||
this.addProperty('DURATION', end); | ||
} | ||
VEvent.prototype.inTimeRange = function(start, end) { | ||
var dtstart = this.getPropertyValue('DTSTART'); | ||
var dtend = this.getPropertyValue('DTEND'); | ||
var rr = this.getPropertyValue('RRULE'); | ||
if(rr) { | ||
rr = new RRule(rr, dtstart, dtend); | ||
var next = rr.next(start); | ||
return (next !== null && (!end || next <= end)); | ||
} | ||
if(!dtend) { | ||
var duration = this.getPropertyValue('DURATION'); | ||
if(duration === 0) | ||
// Special case for zero-duration, as per RFC4791 | ||
return (!start || start <= dtstart) && (!end || end > dtstart); | ||
else if(duration) | ||
dtend = new Date(dtstart.valueOf() + this.getPropertyValue('DURATION')*1000); | ||
else | ||
dtend = new Date(dtstart.valueOf() + 24*60*60*1000); // +1 day | ||
} | ||
return (!start || start < dtend) && (!end || end > dtstart); | ||
} | ||
var VTimezone = exports.VTimezone = function(calendar, tzid) { | ||
CalendarObject.call(this, calendar, 'VTIMEZONE'); | ||
this.addProperty('TZID', tzid); | ||
} | ||
util.inherits(VTimezone, CalendarObject); | ||
VTimezone.prototype.getRRule = function(section) { | ||
var comp = this.getComponents('STANDARD')[0]; | ||
if(!comp || !comp.getPropertyValue('RRULE')) | ||
return null; | ||
return new RRule(comp.getPropertyValue('RRULE'), | ||
comp.getPropertyValue('DTSTART'), | ||
comp.getPropertyValue('DTEND')); | ||
} | ||
VTimezone.prototype.getOffsetForDate = function(dt) { | ||
if(!this.getComponents('DAYLIGHT').length) | ||
return this.getComponents('STANDARD').getPropertyValue('TZOFFSETTO'); | ||
// Right now we're only supporting a single element | ||
assert.equal(1, this.components['STANDARD'].length); | ||
assert.equal(1, this.components['DAYLIGHT'].length); | ||
var next_std = this.getRRule('STANDARD').next(dt); | ||
var next_dst = this.getRRule('DAYLIGHT').next(dt); | ||
// TODO: Using prevOccurs would be a better solution | ||
return this.getComponents(next_std < next_dst ? 'STANDARD' : 'DAYLIGHT')[0] | ||
.getPropertyValue('TZOFFSETTO'); | ||
} | ||
// Convert a parsed date in localtime to a UTC date object | ||
VTimezone.prototype.fromLocalTime = function(dtarray) { | ||
// Create a slightly inaccurate date object | ||
var dt = new Date(dtarray[0], dtarray[1]-1, dtarray[2], | ||
dtarray[3], dtarray[4], dtarray[5]); | ||
var hrs = this.getOffsetForDate(dt); | ||
var min = hrs % 100; | ||
hrs = (hrs-min) / 100; | ||
return new Date(Date.UTC(dtarray[0], dtarray[1]-1, dtarray[2], | ||
dtarray[3]-hrs, dtarray[4]-min, dtarray[5])); | ||
} | ||
// iCalendar schema, required prop | ||
var schema = exports.schema = { | ||
VCALENDAR: { | ||
factory: iCalendar, | ||
valid_properties: [], | ||
required_properties: ['PRODID','VERSION'], | ||
valid_children: ['VEVENT'], | ||
required_children: [] | ||
}, | ||
VEVENT: { | ||
factory: VEvent, | ||
valid_properties: [], | ||
required_properties: ['DTSTAMP','UID'], | ||
valid_children: [], | ||
required_children: [] | ||
}, | ||
VTODO: { | ||
required_properties: ['DTSTAMP','UID'] | ||
}, | ||
VJOURNAL: { | ||
required_properties: ['DTSTAMP','UID'] | ||
}, | ||
VFREEBUSY: { | ||
required_properties: ['DTSTAMP','UID'] | ||
}, | ||
VALARM: { | ||
required_properties: ['ACTION','TRIGGER'] | ||
}, | ||
VTIMEZONE: { | ||
factory: VTimezone, | ||
} | ||
schema.VCALENDAR = { | ||
factory: iCalendar, | ||
valid_properties: [], | ||
required_properties: ['PRODID','VERSION'], | ||
valid_children: ['VEVENT'], | ||
required_children: [] | ||
}; | ||
var properties = exports.properties = { | ||
DTSTAMP: { | ||
type: 'DATE-TIME' | ||
}, | ||
DTSTART: { | ||
params: ['TZID'], | ||
type: 'DATE-TIME' | ||
}, | ||
DTEND: { | ||
type: 'DATE-TIME' | ||
}, | ||
DURATION: { | ||
type: 'DURATION' | ||
}, | ||
RRULE: { | ||
type: 'RECUR' | ||
}, | ||
VERSION: { }, | ||
PRODID: { } | ||
// Unimplemented components... | ||
schema.VTODO = { | ||
required_properties: ['DTSTAMP','UID'] | ||
}; | ||
schema.VJOURNAL = { | ||
required_properties: ['DTSTAMP','UID'] | ||
}; | ||
schema.VFREEBUSY = { | ||
required_properties: ['DTSTAMP','UID'] | ||
}; | ||
schema.VALARM = { | ||
required_properties: ['ACTION','TRIGGER'] | ||
}; |
@@ -27,6 +27,13 @@ // Copyright (C) 2011 Tri Tech Computers Ltd. | ||
var ics = require('./icalendar'); | ||
var CalendarObject = require('./base').CalendarObject; | ||
var schema = require('./base').schema; | ||
var properties = require('./base').properties; | ||
var format_value = exports.format_value = types.format_value; | ||
var iCalendar = require('./icalendar').iCalendar; | ||
var parse_value = types.parse_value; | ||
var format_value = types.format_value; | ||
var ParseError = exports.ParseError = function() { | ||
@@ -57,6 +64,6 @@ Error.apply(this, arguments); | ||
if(element == 'BEGIN') { | ||
var factory = (ics.schema[value] || {}).factory; | ||
var factory = (schema[value] || {}).factory; | ||
var child = factory !== undefined | ||
? new factory(cal) | ||
: new ics.CalendarObject(cal, value); | ||
: new CalendarObject(cal, value); | ||
@@ -76,3 +83,3 @@ return parse_component(child, function() { | ||
else { | ||
value = ics.parse_value((ics.properties[element] || {}).type, | ||
value = parse_value((properties[element] || {}).type, | ||
value, parameters, cal); | ||
@@ -91,3 +98,3 @@ component.addProperty(element, value, parameters); | ||
data = data.split(/\r?\n/); | ||
var calendar = new ics.iCalendar(true); | ||
var calendar = new iCalendar(true); | ||
if(timezone) { | ||
@@ -97,3 +104,3 @@ if(typeof timezone === 'string') | ||
if(timezone instanceof ics.iCalendar) { | ||
if(timezone instanceof iCalendar) { | ||
var tzs = timezone.getComponents('VTIMEZONE'); | ||
@@ -100,0 +107,0 @@ for(var i=0; i<tzs.length; ++i) |
1201
lib/rrule.js
@@ -1,956 +0,461 @@ | ||
/** | ||
* This class is used to determine new for a recurring event, when the next | ||
* events occur. | ||
* | ||
* This iterator may loop infinitely in the future, therefore it is important | ||
* that if you use this class, you set hard limits for the amount of iterations | ||
* you want to handle. | ||
* | ||
* Note that currently there is not full support for the entire iCalendar | ||
* specification, as it's very complex and contains a lot of permutations | ||
* that's not yet used very often in software. | ||
* | ||
* For the focus has been on features as they actually appear in Calendaring | ||
* software, but this may well get expanded as needed / on demand | ||
* | ||
* The following RRULE properties are supported | ||
* * UNTIL | ||
* * INTERVAL | ||
* * FREQ=DAILY | ||
* * BYDAY | ||
* * FREQ=WEEKLY | ||
* * BYDAY | ||
* * WKST | ||
* * FREQ=MONTHLY | ||
* * BYMONTHDAY | ||
* * BYDAY | ||
* * BYSETPOS | ||
* * FREQ=YEARLY | ||
* * BYMONTH | ||
* * BYMONTHDAY (only if BYMONTH is also set) | ||
* * BYDAY (only if BYMONTH is also set) | ||
* | ||
* Anything beyond this is 'undefined', which means that it may get ignored, or | ||
* you may get unexpected results. The effect is that in some applications the | ||
* specified recurrence may look incorrect, or is missing. | ||
* | ||
* @package Sabre | ||
* @subpackage VObject | ||
* @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved. | ||
* @author Evert Pot (http://www.rooftopsolutions.nl/) | ||
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License | ||
*/ | ||
var RRuleIterator = exports.RRuleIterator = function() { | ||
if (is_null($uid)) { | ||
if ($vcal->name === 'VCALENDAR') { | ||
throw new InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well'); | ||
} | ||
$components = array($vcal); | ||
$uid = (string)$vcal->uid; | ||
} else { | ||
$components = $vcal->select('VEVENT'); | ||
} | ||
foreach($components as $component) { | ||
if ((string)$component->uid == $uid) { | ||
if (isset($component->{'RECURRENCE-ID'})) { | ||
$this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component; | ||
$this->exceptionDates[] = $component->{'RECURRENCE-ID'}->getDateTime(); | ||
} else { | ||
$this->baseEvent = $component; | ||
} | ||
} | ||
} | ||
if (!$this->baseEvent) { | ||
throw new InvalidArgumentException('Could not find a base event with uid: ' . $uid); | ||
} | ||
// Copyright (C) 2011 Tri Tech Computers Ltd. | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
// this software and associated documentation files (the "Software"), to deal in | ||
// the Software without restriction, including without limitation the rights to | ||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||
// of the Software, and to permit persons to whom the Software is furnished to do | ||
// so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in all | ||
// copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
// SOFTWARE. | ||
// | ||
// | ||
// | ||
// NB: All calculations here happen using the UTC portion of a datetime object | ||
// as if it were the local time. This is done to reuse the TZ-agnostic date | ||
// calculations provided to us. Without this, performing date calculations | ||
// across local DST boundaries would yield surprising results. | ||
// | ||
$this->startDate = clone $this->baseEvent->DTSTART->getDateTime(); | ||
$this->endDate = null; | ||
if (isset($this->baseEvent->DTEND)) { | ||
$this->endDate = clone $this->baseEvent->DTEND->getDateTime(); | ||
} else { | ||
$this->endDate = clone $this->startDate; | ||
if (isset($this->baseEvent->DURATION)) { | ||
$this->endDate->add(Sabre_VObject_DateTimeParser::parse($this->baseEvent->DURATION->value)); | ||
} | ||
} | ||
$this->currentDate = clone $this->startDate; | ||
var types = require('./types'); | ||
$rrule = (string)$this->baseEvent->RRULE; | ||
var SUPPORTED_PARTS = ['FREQ','INTERVAL','COUNT','UNTIL','BYDAY','BYMONTH','BYMONTHDAY']; | ||
var WKDAYS = ['SU','MO','TU','WE','TH','FR','SA']; | ||
$parts = explode(';', $rrule); | ||
function to_utc_date(dt) { | ||
if(Array.isArray(dt)) { | ||
dt = dt.slice(0); // Make a copy... | ||
dt[1]--; // Fixup month for Date.UTC() | ||
} | ||
else | ||
dt = [dt.getFullYear(), dt.getMonth(), dt.getDate(), | ||
dt.getHours(), dt.getMinutes(), dt.getSeconds(), dt.getMilliseconds()]; | ||
foreach($parts as $part) { | ||
return new Date(Date.UTC.apply(null, dt)); | ||
} | ||
list($key, $value) = explode('=', $part, 2); | ||
function from_utc_date(dt) { | ||
return new Date(dt.getUTCFullYear(), dt.getUTCMonth(), dt.getUTCDate(), | ||
dt.getUTCHours(), dt.getUTCMinutes(), dt.getUTCSeconds(), dt.getUTCMilliseconds()); | ||
} | ||
switch(strtoupper($key)) { | ||
// Return only the whole number portion of a number | ||
function trunc(n) { | ||
return n < 0 ? Math.ceil(n) : Math.floor(n); | ||
} | ||
case 'FREQ' : | ||
if (!in_array( | ||
strtolower($value), | ||
array('secondly','minutely','hourly','daily','weekly','monthly','yearly') | ||
)) { | ||
throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value)); | ||
// These are more comfy to type... | ||
function y(dt) { return dt.getUTCFullYear(); } | ||
function m(dt) { return dt.getUTCMonth()+1; } | ||
function d(dt) { return dt.getUTCDate(); } | ||
function hr(dt) { return dt.getUTCHours(); } | ||
function min(dt) { return dt.getUTCMinutes(); } | ||
function sec(dt) { return dt.getUTCSeconds(); } | ||
} | ||
$this->frequency = strtolower($value); | ||
break; | ||
function set_y(dt, v) { dt.setUTCFullYear(v); return dt; } | ||
function set_m(dt, v) { dt.setUTCMonth(v-1); return dt; } | ||
function set_d(dt, v) { dt.setUTCDate(v); return dt; } | ||
function set_hr(dt, v) { dt.setUTCHours(v); return dt; } | ||
function set_min(dt, v) { dt.setUTCMinutes(v); return dt; } | ||
function set_sec(dt, v) { dt.setUTCSeconds(v); return dt; } | ||
case 'UNTIL' : | ||
$this->until = Sabre_VObject_DateTimeParser::parse($value); | ||
break; | ||
function add_y(dt, v) { return set_y(dt, y(dt)+v); } | ||
function add_m(dt, v) { return set_m(dt, m(dt)+v); } | ||
function add_d(dt, v) { return set_d(dt, d(dt)+v); } | ||
function add_hr(dt, v) { return set_hr(dt, hr(dt)+v); } | ||
function add_min(dt, v) { return set_min(dt, min(dt)+v); } | ||
function add_sec(dt, v) { return set_sec(dt, sec(dt)+v); } | ||
case 'COUNT' : | ||
$this->count = (int)$value; | ||
break; | ||
// First of the month | ||
function fst(dt) { | ||
return new Date(y(dt), m(dt)-1, 1); | ||
} | ||
case 'INTERVAL' : | ||
$this->interval = (int)$value; | ||
break; | ||
// Day of week (0-6), adjust for the start of week | ||
function wkday(dt) { | ||
return dt.getUTCDay(); | ||
} | ||
case 'BYSECOND' : | ||
$this->bySecond = explode(',', $value); | ||
break; | ||
// Return the number of days between dt1 and dt2 | ||
function daydiff(dt1, dt2) { | ||
return (dt2-dt1)/(1000*60*60*24); | ||
} | ||
case 'BYMINUTE' : | ||
$this->byMinute = explode(',', $value); | ||
break; | ||
// Week of year | ||
function wk(dt) { | ||
var jan1 = new Date(Date.UTC(y(dt), 0, 1)); | ||
return trunc(daydiff(jan1, dt)/7); | ||
} | ||
case 'BYHOUR' : | ||
$this->byHour = explode(',', $value); | ||
break; | ||
// Week of month | ||
function m_wk(dt, wkst) { | ||
return (0 | d(dt)/7) + (d(dt) % 7 === 0 ? 0 : 1); | ||
} | ||
case 'BYDAY' : | ||
$this->byDay = explode(',', strtoupper($value)); | ||
break; | ||
case 'BYMONTHDAY' : | ||
$this->byMonthDay = explode(',', $value); | ||
break; | ||
var RRule = exports.RRule = function(rule, start, end) { | ||
this.start = start ? to_utc_date(start) : null; | ||
this.end = end ? to_utc_date(end) : null; | ||
case 'BYYEARDAY' : | ||
$this->byYearDay = explode(',', $value); | ||
break; | ||
if(typeof rule === 'string') | ||
rule = RRule.parse(rule); | ||
case 'BYWEEKNO' : | ||
$this->byWeekNo = explode(',', $value); | ||
break; | ||
this.rule = {}; | ||
for(var i in (rule||{})) { | ||
if(SUPPORTED_PARTS.indexOf(i) == -1) | ||
throw new Error(i+" is not currently supported!"); | ||
case 'BYMONTH' : | ||
$this->byMonth = explode(',', $value); | ||
break; | ||
case 'BYSETPOS' : | ||
$this->bySetPos = explode(',', $value); | ||
break; | ||
case 'WKST' : | ||
$this->weekStart = strtoupper($value); | ||
break; | ||
} | ||
this.rule[i] = RULE_PARTS[i] | ||
? RULE_PARTS[i].parse(rule[i]) | ||
: rule[i]; | ||
} | ||
} | ||
// Parsing exception dates | ||
if (isset($this->baseEvent->EXDATE)) { | ||
foreach($this->baseEvent->EXDATE as $exDate) { | ||
foreach(explode(',', (string)$exDate) as $exceptionDate) { | ||
$this->exceptionDates[] = | ||
Sabre_VObject_DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone()); | ||
} | ||
} | ||
RRule.parse = function(value) { | ||
var parts = value.split(/=|;/); | ||
var rrule = {}; | ||
for(var i=0; i<parts.length; i+=2) { | ||
rrule[parts[i]] = parts[i+1]; | ||
} | ||
return rrule; | ||
} | ||
RRule.prototype.setFrequency = function(freq) { | ||
this.rule.FREQ = freq; | ||
} | ||
class Sabre_VObject_RecurrenceIterator implements Iterator { | ||
RRule.prototype.valueOf = function() { return this.rule; } | ||
/** | ||
* The initial event date | ||
* | ||
* @var DateTime | ||
*/ | ||
public $startDate; | ||
RRule.prototype.toString = function() { | ||
// FREQ comes first, as per spec | ||
var out = [ 'FREQ='+this.rule.FREQ ]; | ||
for(var k in this.rule) { | ||
if(k=='FREQ') continue; | ||
/** | ||
* The end-date of the initial event | ||
* | ||
* @var DateTime | ||
*/ | ||
public $endDate; | ||
/** | ||
* The 'current' recurrence. | ||
* | ||
* This will be increased for every iteration. | ||
* | ||
* @var DateTime | ||
*/ | ||
public $currentDate; | ||
/** | ||
* List of dates that are excluded from the rules. | ||
* | ||
* This list contains both the items that are specified in the EXDATE | ||
* property, as well as the ones that have been overridden by other events | ||
* and RECURRENCE-ID. | ||
* | ||
* @var array | ||
*/ | ||
public $exceptionDates = array(); | ||
/** | ||
* Base event | ||
* | ||
* @var Sabre_VObject_Component_VEvent | ||
*/ | ||
public $baseEvent; | ||
/** | ||
* list of events that are 'overridden'. | ||
* | ||
* This is an array of Sabre_VObject_Component_VEvent objects. | ||
* | ||
* @var array | ||
*/ | ||
public $overriddenEvents = array(); | ||
/** | ||
* Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, | ||
* yearly. | ||
* | ||
* @var string | ||
*/ | ||
public $frequency; | ||
/** | ||
* The last instance of this recurrence, inclusively | ||
* | ||
* @var DateTime|null | ||
*/ | ||
public $until; | ||
/** | ||
* The number of recurrences, or 'null' if infinitely recurring. | ||
* | ||
* @var int | ||
*/ | ||
public $count; | ||
/** | ||
* The interval. | ||
* | ||
* If for example frequency is set to daily, interval = 2 would mean every | ||
* 2 days. | ||
* | ||
* @var int | ||
*/ | ||
public $interval = 1; | ||
/** | ||
* Which seconds to recur. | ||
* | ||
* This is an array of integers (between 0 and 60) | ||
* | ||
* @var array | ||
*/ | ||
public $bySecond; | ||
/** | ||
* Which minutes to recur | ||
* | ||
* This is an array of integers (between 0 and 59) | ||
* | ||
* @var array | ||
*/ | ||
public $byMinute; | ||
/** | ||
* Which hours to recur | ||
* | ||
* This is an array of integers (between 0 and 23) | ||
* | ||
* @var array | ||
*/ | ||
public $byHour; | ||
/** | ||
* Which weekdays to recur. | ||
* | ||
* This is an array of weekdays | ||
* | ||
* This may also be preceeded by a positive or negative integer. If present, | ||
* this indicates the nth occurrence of a specific day within the monthly or | ||
* yearly rrule. For instance, -2TU indicates the second-last tuesday of | ||
* the month, or year. | ||
* | ||
* @var array | ||
*/ | ||
public $byDay; | ||
/** | ||
* Which days of the month to recur | ||
* | ||
* This is an array of days of the months (1-31). The value can also be | ||
* negative. -5 for instance means the 5th last day of the month. | ||
* | ||
* @var array | ||
*/ | ||
public $byMonthDay; | ||
/** | ||
* Which days of the year to recur. | ||
* | ||
* This is an array with days of the year (1 to 366). The values can also | ||
* be negative. For instance, -1 will always represent the last day of the | ||
* year. (December 31st). | ||
* | ||
* @var array | ||
*/ | ||
public $byYearDay; | ||
/** | ||
* Which week numbers to recur. | ||
* | ||
* This is an array of integers from 1 to 53. The values can also be | ||
* negative. -1 will always refer to the last week of the year. | ||
* | ||
* @var array | ||
*/ | ||
public $byWeekNo; | ||
/** | ||
* Which months to recur | ||
* | ||
* This is an array of integers from 1 to 12. | ||
* | ||
* @var array | ||
*/ | ||
public $byMonth; | ||
/** | ||
* Which items in an existing st to recur. | ||
* | ||
* These numbers work together with an existing by* rule. It specifies | ||
* exactly which items of the existing by-rule to filter. | ||
* | ||
* Valid values are 1 to 366 and -1 to -366. As an example, this can be | ||
* used to recur the last workday of the month. | ||
* | ||
* This would be done by setting frequency to 'monthly', byDay to | ||
* 'MO,TU,WE,TH,FR' and bySetPos to -1. | ||
* | ||
* @var array | ||
*/ | ||
public $bySetPos; | ||
/** | ||
* When a week starts | ||
* | ||
* @var string | ||
*/ | ||
public $weekStart = 'MO'; | ||
/** | ||
* The current item in the list | ||
* | ||
* @var int | ||
*/ | ||
public $counter = 0; | ||
/** | ||
* Simple mapping from iCalendar day names to day numbers | ||
* | ||
* @var array | ||
*/ | ||
private $dayMap = array( | ||
'SU' => 0, | ||
'MO' => 1, | ||
'TU' => 2, | ||
'WE' => 3, | ||
'TH' => 4, | ||
'FR' => 5, | ||
'SA' => 6, | ||
); | ||
/** | ||
* Mappings between the day number and english day name. | ||
* | ||
* @var array | ||
*/ | ||
private $dayNames = array( | ||
0 => 'Sunday', | ||
1 => 'Monday', | ||
2 => 'Tuesday', | ||
3 => 'Wednesday', | ||
4 => 'Thursday', | ||
5 => 'Friday', | ||
6 => 'Saturday', | ||
); | ||
/** | ||
* If the current iteration of the event is an overriden event, this | ||
* property will hold the VObject | ||
* | ||
* @var Sabre_Component_VObject | ||
*/ | ||
private $currentOverriddenEvent; | ||
/** | ||
* This property may contain the date of the next not-overridden event. | ||
* This date is calculated sometimes a bit early, before overridden events | ||
* are evaluated. | ||
* | ||
* @var DateTime | ||
*/ | ||
private $nextDate; | ||
/** | ||
* Creates the iterator | ||
* | ||
* You should pass a VCALENDAR component, as well as the UID of the event | ||
* we're going to traverse. | ||
* | ||
* @param Sabre_VObject_Component $comp | ||
*/ | ||
public function __construct(Sabre_VObject_Component $vcal, $uid=null) { | ||
out.push(k+'='+((RULE_PARTS[k] || {}).format | ||
? RULE_PARTS[k].format(this.rule[k]) | ||
: this.rule[k])); | ||
} | ||
return out.join(';'); | ||
} | ||
/** | ||
* Returns the current item in the list | ||
* | ||
* @return DateTime | ||
*/ | ||
public function current() { | ||
// Return the next occurrence after dt | ||
RRule.prototype.next = function(after) { | ||
after = after && to_utc_date(after); | ||
if (!$this->valid()) return null; | ||
return clone $this->currentDate; | ||
// Events don't occur before the start or after the end... | ||
if(!after || after < this.start) | ||
return from_utc_date(this.start); | ||
if(this.end && after > this.end) return null; | ||
} | ||
var freq = FREQ[this.rule.FREQ]; | ||
if(!freq) | ||
throw new Error(this.rule.FREQ+' recurrence is not supported'); | ||
/** | ||
* This method returns the startdate for the current iteration of the | ||
* event. | ||
* | ||
* @return DateTime | ||
*/ | ||
public function getDtStart() { | ||
var next = freq.next(this.rule, this.start, after); | ||
return clone $this->currentDate; | ||
// Date is off the end of the spectrum... | ||
if(this.end && next > this.end) | ||
return null; | ||
} | ||
/** | ||
* This method returns the enddate for the current iteration of the | ||
* event. | ||
* | ||
* @return DateTime | ||
*/ | ||
public function getDtEnd() { | ||
$dtEnd = clone $this->currentDate; | ||
$dtEnd->add( $this->startDate->diff( $this->endDate ) ); | ||
return clone $dtEnd; | ||
} | ||
/** | ||
* Returns a VEVENT object with the updated start and end date. | ||
* | ||
* Any recurrence information is removed, and this function may return an | ||
* 'overridden' event instead. | ||
* | ||
* This method always returns a cloned instance. | ||
* | ||
* @return void | ||
*/ | ||
public function getEventObject() { | ||
if ($this->currentOverriddenEvent) { | ||
return clone $this->currentOverriddenEvent; | ||
if(this.rule.COUNT && this.count_end !== null) { | ||
if(this.count_end === undefined) { | ||
// Don't check this while we're trying to compute it... | ||
this.count_end = null; | ||
this.count_end = this.nextOccurences(this.rule.COUNT).pop(); | ||
} | ||
$event = clone $this->baseEvent; | ||
unset($event->RRULE); | ||
unset($event->EXDATE); | ||
unset($event->RDATE); | ||
unset($event->EXRULE); | ||
$event->DTSTART->setDateTime($this->currentDate, $event->DTSTART->getDateType()); | ||
if (isset($event->DTEND)) { | ||
$event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType()); | ||
} | ||
if ($this->counter > 0) { | ||
$event->{'RECURRENCE-ID'} = (string)$event->DTSTART; | ||
} | ||
return $event; | ||
if(next > this.count_end) | ||
return null; | ||
} | ||
/** | ||
* Returns the current item number | ||
* | ||
* @return int | ||
*/ | ||
public function key() { | ||
if(this.rule.UNTIL && next > this.rule.UNTIL) | ||
return null; | ||
return $this->counter; | ||
return from_utc_date(next); | ||
} | ||
RRule.prototype.nextOccurences = function(after, count) { | ||
if(arguments.length === 1) { | ||
count = after; | ||
after = undefined; | ||
} | ||
/** | ||
* Whether or not there is a 'next item' | ||
* | ||
* @return bool | ||
*/ | ||
public function valid() { | ||
if (!is_null($this->count)) { | ||
return $this->counter < $this->count; | ||
} | ||
if (!is_null($this->until)) { | ||
return $this->currentDate <= $this->until; | ||
} | ||
return true; | ||
var arr = []; | ||
while(count-- && after !== null) { | ||
after = this.next(after); | ||
if(after) | ||
arr.push(after); | ||
} | ||
return arr; | ||
} | ||
/** | ||
* Resets the iterator | ||
* | ||
* @return void | ||
*/ | ||
public function rewind() { | ||
$this->currentDate = clone $this->startDate; | ||
$this->counter = 0; | ||
var RULE_PARTS = { | ||
INTERVAL: { | ||
parse: function(v) { return parseInt(v,10); } | ||
}, | ||
UNTIL: { | ||
parse: function(v) { return types.parse_value('DATE-TIME', v); }, | ||
format: function(v) { return types.format_value('DATE-TIME', v); } | ||
}, | ||
FREQ: { | ||
parse: function(v) { return v; }, | ||
}, | ||
BYMONTH: { | ||
parse: function(v) { | ||
if(typeof v === 'number') return [v]; | ||
} | ||
return v.split(',').map(function(mo) { | ||
return parseInt(mo,10); | ||
}); | ||
}, | ||
format: function(v) { | ||
return v.join(','); | ||
} | ||
}, | ||
BYDAY: { // 2TH (second thursday) -> [2,4] | ||
parse: function(v) { | ||
var days = v.split(',').map(function(day) { | ||
var m = day.match(/([+-]?\d)?(SU|MO|TU|WE|TH|FR|SA)/); | ||
return [parseInt(m[1],10)||0, WKDAYS.indexOf(m[2])]; | ||
}); | ||
/** | ||
* This method allows you to quickly go to the next occurrence after the | ||
* specified date. | ||
* | ||
* Note that this checks the current 'endDate', not the 'stardDate'. This | ||
* means that if you forward to January 1st, the iterator will stop at the | ||
* first event that ends *after* January 1st. | ||
* | ||
* @param DateTime $dt | ||
* @return void | ||
*/ | ||
public function fastForward(DateTime $dt) { | ||
days.sort(function(d1, d2) { | ||
// Sort by week, day of week | ||
if(d1[0] == d2[0]) | ||
return d1[1] - d2[1]; | ||
else | ||
return d1[0] - d2[0]; | ||
}); | ||
while($this->valid() && $this->getDTEnd() < $dt) { | ||
$this->next(); | ||
return days; | ||
}, | ||
format: function(v) { | ||
return v.map(function(day) { | ||
return (day[0] || '')+WKDAYS[day[1]]; | ||
}).join(','); | ||
} | ||
} | ||
}; | ||
/** | ||
* Goes on to the next iteration | ||
* | ||
* @return void | ||
*/ | ||
public function next() { | ||
// These parts use the same format... | ||
RULE_PARTS['BYMONTHDAY'] = RULE_PARTS['BYMONTH']; | ||
RULE_PARTS['COUNT'] = RULE_PARTS['INTERVAL']; | ||
$previousStamp = $this->currentDate->getTimeStamp(); | ||
var FREQ = { | ||
DAILY: { | ||
next: function(rule, start, after) { | ||
var next = new Date(after); | ||
var interval = rule.INTERVAL || 1; | ||
while(true) { | ||
// Adjust for interval... | ||
var mod_days = daydiff(next, start) % interval; | ||
if(mod_days) | ||
add_d(next, interval - mod_days); | ||
$this->currentOverriddenEvent = null; | ||
for(var i=0; i<2; ++i) { | ||
next = byday(rule.BYDAY, next); | ||
set_hr(next, hr(start)); | ||
set_min(next, min(start)); | ||
set_sec(next, sec(start)); | ||
// If we have a next date 'stored', we use that | ||
if ($this->nextDate) { | ||
$this->currentDate = $this->nextDate; | ||
$currentStamp = $this->currentDate->getTimeStamp(); | ||
$this->nextDate = null; | ||
} else { | ||
if(after.valueOf() != next.valueOf()) | ||
break; | ||
// Otherwise, we calculate it | ||
switch($this->frequency) { | ||
case 'daily' : | ||
$this->nextDaily(); | ||
break; | ||
case 'weekly' : | ||
$this->nextWeekly(); | ||
break; | ||
case 'monthly' : | ||
$this->nextMonthly(); | ||
break; | ||
case 'yearly' : | ||
$this->nextYearly(); | ||
break; | ||
} | ||
$currentStamp = $this->currentDate->getTimeStamp(); | ||
// Checking exception dates | ||
foreach($this->exceptionDates as $exceptionDate) { | ||
if ($this->currentDate == $exceptionDate) { | ||
continue 2; | ||
} | ||
} | ||
add_d(next, interval); | ||
} | ||
// Checking overriden events | ||
foreach($this->overriddenEvents as $index=>$event) { | ||
if ($index > $previousStamp && $index < $currentStamp) { | ||
// We're moving the 'next date' aside, for later use. | ||
$this->nextDate = clone $this->currentDate; | ||
$this->currentDate = $event->DTSTART->getDateTime(); | ||
$this->currentOverriddenEvent = $event; | ||
break; | ||
} | ||
} | ||
break; | ||
return next; | ||
} | ||
}, | ||
WEEKLY: { | ||
next: function(rule, start, after) { | ||
var next = new Date(after); | ||
var interval = rule.INTERVAL || 1; | ||
$this->counter++; | ||
// Adjust for interval... | ||
var days = daydiff(start, next); | ||
if(days) add_d(next, 7 - (days % 7)); | ||
} | ||
var mod_weeks = Math.floor(daydiff(next, start) / 7) % interval; | ||
if(mod_weeks) | ||
add_d(next, (interval - mod_weeks) * 7); | ||
/** | ||
* Does the processing for advancing the iterator for daily frequency. | ||
* | ||
* @return void | ||
*/ | ||
protected function nextDaily() { | ||
while(true) { | ||
next = byday(rule.BYDAY, next); | ||
set_hr(next, hr(start)); | ||
set_min(next, min(start)); | ||
set_sec(next, sec(start)); | ||
if (!$this->byDay) { | ||
$this->currentDate->modify('+' . $this->interval . ' days'); | ||
return; | ||
} | ||
if(after.valueOf() != next.valueOf() | ||
&& check_bymonth(rule.BYMONTH, next)) | ||
break; | ||
$recurrenceDays = array(); | ||
foreach($this->byDay as $byDay) { | ||
add_d(next, interval * 7); | ||
} | ||
// The day may be preceeded with a positive (+n) or | ||
// negative (-n) integer. However, this does not make | ||
// sense in 'weekly' so we ignore it here. | ||
$recurrenceDays[] = $this->dayMap[substr($byDay,-2)]; | ||
return next; | ||
} | ||
}, | ||
MONTHLY: { | ||
next: function(rule, start, after) { | ||
var next = new Date(after); | ||
var interval = rule.INTERVAL || 1; | ||
do { | ||
// Adjust interval to be correct | ||
var delta = (m(next) - m(start)) + (y(next) - y(start)) * 12; | ||
if(delta % interval) | ||
add_m(next, interval - (delta % interval)); | ||
$this->currentDate->modify('+' . $this->interval . ' days'); | ||
for(var i=0; i<2; ++i) { | ||
next = byday(rule.BYDAY, next); | ||
next = bymonthday(rule.BYMONTHDAY, next); | ||
set_hr(next, hr(start)); | ||
set_min(next, min(start)); | ||
set_sec(next, sec(start)); | ||
// Current day of the week | ||
$currentDay = $this->currentDate->format('w'); | ||
if(after.valueOf() != next.valueOf()) | ||
break; | ||
} while (!in_array($currentDay, $recurrenceDays)); | ||
} | ||
/** | ||
* Does the processing for advancing the iterator for weekly frequency. | ||
* | ||
* @return void | ||
*/ | ||
protected function nextWeekly() { | ||
if (!$this->byDay) { | ||
$this->currentDate->modify('+' . $this->interval . ' weeks'); | ||
return; | ||
} | ||
$recurrenceDays = array(); | ||
foreach($this->byDay as $byDay) { | ||
// The day may be preceeded with a positive (+n) or | ||
// negative (-n) integer. However, this does not make | ||
// sense in 'weekly' so we ignore it here. | ||
$recurrenceDays[] = $this->dayMap[substr($byDay,-2)]; | ||
} | ||
// Current day of the week | ||
$currentDay = $this->currentDate->format('w'); | ||
// First day of the week: | ||
$firstDay = $this->dayMap[$this->weekStart]; | ||
// Increasing the 'current day' until we find our next | ||
// occurrence. | ||
while(true) { | ||
$currentDay++; | ||
if ($currentDay>6) { | ||
$currentDay = 0; | ||
add_m(next, interval); | ||
} | ||
// We need to roll over to the next week | ||
if ($currentDay === $firstDay) { | ||
$this->currentDate->modify('+' . $this->interval . ' weeks'); | ||
$this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]); | ||
} | ||
// We have a match | ||
if (in_array($currentDay ,$recurrenceDays)) { | ||
$this->currentDate->modify($this->dayNames[$currentDay]); | ||
break; | ||
} | ||
return next; | ||
} | ||
}, | ||
YEARLY: { | ||
next: function(rule, start, after) { | ||
// Occurs every N years... | ||
var next = new Date(after); | ||
var interval = rule.INTERVAL || 1; | ||
} | ||
var mod_year = (y(after) - y(start)) % interval; | ||
if(mod_year) | ||
// We're not in a valid year, move to the next valid year | ||
add_y(next, interval - mod_year); | ||
/** | ||
* Does the processing for advancing the iterator for monthly frequency. | ||
* | ||
* @return void | ||
*/ | ||
protected function nextMonthly() { | ||
$currentDayOfMonth = $this->currentDate->format('j'); | ||
if (!$this->byMonthDay && !$this->byDay) { | ||
for(var i=0; i<2; ++i) { | ||
next = bymonth(rule.BYMONTH || m(start), next, i); | ||
next = bymonthday(rule.BYMONTHDAY, next); | ||
next = byday(rule.BYDAY, next, i); | ||
// If the current day is higher than the 28th, rollover can | ||
// occur to the next month. We Must skip these invalid | ||
// entries. | ||
if ($currentDayOfMonth < 29) { | ||
$this->currentDate->modify('+' . $this->interval . ' months'); | ||
} else { | ||
$increase = 0; | ||
do { | ||
$increase++; | ||
$tempDate = clone $this->currentDate; | ||
$tempDate->modify('+ ' . ($this->interval*$increase) . ' months'); | ||
} while ($tempDate->format('j') != $currentDayOfMonth); | ||
$this->currentDate = $tempDate; | ||
} | ||
return; | ||
} | ||
// TODO: Add actual byhour/minute/second methods | ||
set_hr(next, hr(start)); | ||
set_min(next, min(start)); | ||
set_sec(next, sec(start)); | ||
while(true) { | ||
// Don't loop back again if we found a new date | ||
if(after.valueOf() != next.valueOf()) | ||
break; | ||
$occurrences = $this->getMonthlyOccurrences(); | ||
foreach($occurrences as $occurrence) { | ||
// The first occurrence thats higher than the current | ||
// day of the month wins. | ||
if ($occurrence > $currentDayOfMonth) { | ||
break 2; | ||
} | ||
set_d(set_m(add_y(next, interval), 1), 1); | ||
} | ||
// If we made it all the way here, it means there were no | ||
// valid occurrences, and we need to advance to the next | ||
// month. | ||
$this->currentDate->modify('first day of this month'); | ||
$this->currentDate->modify('+ ' . $this->interval . ' months'); | ||
// This goes to 0 because we need to start counting at hte | ||
// beginning. | ||
$currentDayOfMonth = 0; | ||
return next; | ||
} | ||
$this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence); | ||
} | ||
}; | ||
/** | ||
* Does the processing for advancing the iterator for yearly frequency. | ||
* | ||
* @return void | ||
*/ | ||
protected function nextYearly() { | ||
function sort_dates(dateary) { | ||
return dateary.sort(function(dt1, dt2) { | ||
if(dt1 === null && dt2 === null) return 0; | ||
if(dt1 === null) return 1; | ||
if(dt2 === null) return -1; | ||
if (!$this->byMonth) { | ||
$this->currentDate->modify('+' . $this->interval . ' years'); | ||
return; | ||
} | ||
return dt1.valueOf() - dt2.valueOf(); | ||
}); | ||
} | ||
$currentMonth = $this->currentDate->format('n'); | ||
$currentYear = $this->currentDate->format('Y'); | ||
$currentDayOfMonth = $this->currentDate->format('j'); | ||
// Check that a particular date is within the limits | ||
// designated by the BYMONTH rule | ||
function check_bymonth(rules, dt) { | ||
if(!rules || !rules.length) return true; | ||
return rules.indexOf(m(dt)) !== -1; | ||
} | ||
// If we got a byDay or getMonthDay filter, we must first expand | ||
// further. | ||
if ($this->byDay || $this->byMonthDay) { | ||
// Advance to the next month that satisfies the rule... | ||
function bymonth(rules, dt) { | ||
if(!rules || !rules.length) return dt; | ||
while(true) { | ||
var candidates = rules.map(function(rule) { | ||
var delta = rule-m(dt); | ||
if(delta < 0) delta += 12; | ||
$occurrences = $this->getMonthlyOccurrences(); | ||
var newdt = add_m(new Date(dt), delta); | ||
set_d(newdt, 1); | ||
return newdt; | ||
}); | ||
var newdt = sort_dates(candidates).shift(); | ||
return newdt || dt; | ||
} | ||
foreach($occurrences as $occurrence) { | ||
// The first occurrence that's higher than the current | ||
// day of the month wins. | ||
if ($occurrence > $currentDayOfMonth) { | ||
break 2; | ||
} | ||
function bymonthday(rules, dt) { | ||
if(!rules || !rules.length) return dt; | ||
} | ||
var candidates = rules.map(function(rule) { | ||
var delta = rule-m(dt); | ||
if(delta < 0) delta += 12; | ||
// If we made it here, it means we need to advance to | ||
// the next month or year. | ||
$currentDayOfMonth = 1; | ||
do { | ||
var newdt = set_d(new Date(dt), rule); | ||
return (newdt < dt ? null : newdt); | ||
}); | ||
$currentMonth++; | ||
if ($currentMonth>12) { | ||
$currentYear+=$this->interval; | ||
$currentMonth = 1; | ||
} | ||
} while (!in_array($currentMonth, $this->byMonth)); | ||
var newdt = sort_dates(candidates).shift(); | ||
return newdt || dt; | ||
} | ||
$this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); | ||
} | ||
// Advance to the next day that satisfies the byday rule... | ||
function byday(rules, dt) { | ||
if(!rules || !rules.length) return dt; | ||
// If we made it here, it means we got a valid occurrence | ||
$this->currentDate->setDate($currentYear, $currentMonth, $occurrence); | ||
return; | ||
// Generate a list of candiDATES. (HA!) | ||
var candidates = rules.map(function(rule) { | ||
// Align on the correct day of the week... | ||
var days = rule[1]-wkday(dt); | ||
if(days < 0) days += 7; | ||
var newdt = add_d(new Date(dt), days); | ||
} else { | ||
if(rule[0] > 0) { | ||
var wk = 0 | (d(newdt) / 7) + 1; | ||
if(wk > rule[0]) return null; | ||
// no byDay or byMonthDay, so we can just loop through the | ||
// months. | ||
do { | ||
$currentMonth++; | ||
if ($currentMonth>12) { | ||
$currentYear+=$this->interval; | ||
$currentMonth = 1; | ||
} | ||
} while (!in_array($currentMonth, $this->byMonth)); | ||
$this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth); | ||
return; | ||
add_d(newdt, (rule[0] - wk) * 7); | ||
} | ||
} | ||
/** | ||
* Returns all the occurrences for a monthly frequency with a 'byDay' or | ||
* 'byMonthDay' expansion for the current month. | ||
* | ||
* The returned list is an array of integers with the day of month (1-31). | ||
* | ||
* @return array | ||
*/ | ||
protected function getMonthlyOccurrences() { | ||
$startDate = clone $this->currentDate; | ||
$byDayResults = array(); | ||
// Our strategy is to simply go through the byDays, advance the date to | ||
// that point and add it to the results. | ||
if ($this->byDay) foreach($this->byDay as $day) { | ||
$dayName = $this->dayNames[$this->dayMap[substr($day,-2)]]; | ||
// Dayname will be something like 'wednesday'. Now we need to find | ||
// all wednesdays in this month. | ||
$dayHits = array(); | ||
$checkDate = clone $startDate; | ||
$checkDate->modify('first day of this month'); | ||
$checkDate->modify($dayName); | ||
do { | ||
$dayHits[] = $checkDate->format('j'); | ||
$checkDate->modify('next ' . $dayName); | ||
} while ($checkDate->format('n') === $startDate->format('n')); | ||
// So now we have 'all wednesdays' for month. It is however | ||
// possible that the user only really wanted the 1st, 2nd or last | ||
// wednesday. | ||
if (strlen($day)>2) { | ||
$offset = (int)substr($day,0,-2); | ||
if ($offset>0) { | ||
$byDayResults[] = $dayHits[$offset-1]; | ||
} else { | ||
// if it was negative we count from the end of the array | ||
$byDayResults[] = $dayHits[count($dayHits) + $offset]; | ||
} | ||
} else { | ||
// There was no counter (first, second, last wednesdays), so we | ||
// just need to add the all to the list). | ||
$byDayResults = array_merge($byDayResults, $dayHits); | ||
else if(rule[0] < 0) { | ||
// Find all the matching days in the month... | ||
var dt2 = new Date(newdt); | ||
var days = []; | ||
while(m(dt2) === m(newdt)) { | ||
days.push(d(dt2)); | ||
add_d(dt2, 7); | ||
} | ||
// Then grab the nth from the end... | ||
set_d(newdt, days.reverse()[(-rule[0])-1]); | ||
} | ||
$byMonthDayResults = array(); | ||
if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) { | ||
// Don't look outside the current month... | ||
if(m(newdt) !== m(dt)) return null; | ||
// Removing values that are out of range for this month | ||
if ($monthDay > $startDate->format('t') || | ||
$monthDay < 0-$startDate->format('t')) { | ||
continue; | ||
} | ||
if ($monthDay>0) { | ||
$byMonthDayResults[] = $monthDay; | ||
} else { | ||
// Negative values | ||
$byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; | ||
} | ||
} | ||
return newdt; | ||
}); | ||
// If there was just byDay or just byMonthDay, they just specify our | ||
// (almost) final list. If both were provided, then byDay limits the | ||
// list. | ||
if ($this->byMonthDay && $this->byDay) { | ||
$result = array_intersect($byMonthDayResults, $byDayResults); | ||
} elseif ($this->byMonthDay) { | ||
$result = $byMonthDayResults; | ||
} else { | ||
$result = $byDayResults; | ||
} | ||
$result = array_unique($result); | ||
sort($result, SORT_NUMERIC); | ||
// The last thing that needs checking is the BYSETPOS. If it's set, it | ||
// means only certain items in the set survive the filter. | ||
if (!$this->bySetPos) { | ||
return $result; | ||
} | ||
$filteredResult = array(); | ||
foreach($this->bySetPos as $setPos) { | ||
if ($setPos<0) { | ||
$setPos = count($result)-($setPos+1); | ||
} | ||
if (isset($result[$setPos-1])) { | ||
$filteredResult[] = $result[$setPos-1]; | ||
} | ||
} | ||
sort($filteredResult, SORT_NUMERIC); | ||
return $filteredResult; | ||
} | ||
// Select the date occurring next... | ||
var newdt = sort_dates(candidates).shift(); | ||
return newdt || dt; | ||
} | ||
@@ -23,3 +23,3 @@ // Copyright (C) 2011 Tri Tech Computers Ltd. | ||
var RRule = require('./recur').RRule; | ||
var RRule = require('./rrule').RRule; | ||
@@ -26,0 +26,0 @@ function pad(n,d) { |
{ | ||
"name": "icalendar", | ||
"version": "0.4.1", | ||
"version": "0.5.0", | ||
"author": "James Emerton <james@tri-tech.com>", | ||
@@ -13,3 +13,3 @@ "description": "RFC5545 iCalendar parser/generator", | ||
}, | ||
"main": "lib/icalendar", | ||
"main": "lib/index", | ||
"devDependencies": { | ||
@@ -16,0 +16,0 @@ "jasmine-node": ">=1.0.13" |
@@ -6,3 +6,3 @@ // Test search | ||
var icalendar = require('../lib/icalendar'); | ||
var icalendar = require('../lib'); | ||
@@ -94,7 +94,18 @@ // NB: Ported to jasmine from expresso, hence the strange layout | ||
assert.equal(-400, tz.getOffsetForDate(new Date(2011,6,2))); | ||
// These are easy... | ||
expect(tz.getOffsetForDate(new Date(2011,6,2))).toEqual(-400); | ||
expect(tz.getOffsetForDate(new Date(2011,1,2))).toEqual(-500); | ||
// Do we handle transitions correctly? | ||
// NB: These come in as an array because Date objects can't | ||
// represent a time that doesn't actually exist | ||
expect(tz.getOffsetForDate([2011,3,13,1,59,59])).toEqual(-500); | ||
expect(tz.getOffsetForDate([2011,3,13,2,0,0])).toEqual(-400); | ||
expect(tz.getOffsetForDate([2011,11,6,1,59,59])).toEqual(-400); | ||
expect(tz.getOffsetForDate([2011,11,6,2,0,0])).toEqual(-500); | ||
}); | ||
it('creates calendar clones', function() { | ||
var cal = icalendar.iCalendar.parse( | ||
var cal = icalendar.parse_calendar( | ||
'BEGIN:VCALENDAR\r\n'+ | ||
@@ -101,0 +112,0 @@ 'PRODID:-//Bobs Software Emporium//NONSGML Bobs Calendar//EN\r\n'+ |
@@ -6,7 +6,7 @@ // Test search | ||
var icalendar = require('../lib/icalendar'); | ||
var parse_calendar = require('../lib/parser').parse_calendar; | ||
describe("iCalendar.parse", function() { | ||
it('parses data correctly', function() { | ||
var cal = icalendar.iCalendar.parse( | ||
var cal = parse_calendar( | ||
'BEGIN:VCALENDAR\r\n'+ | ||
@@ -36,3 +36,3 @@ 'PRODID:-//Bobs Software Emporium//NONSGML Bobs Calendar//EN\r\n'+ | ||
it('parses large collections', function() { | ||
var cal = icalendar.iCalendar.parse( | ||
var cal = parse_calendar( | ||
fs.readFileSync(__dirname+'/icalendar-test.ics', 'utf8')); | ||
@@ -60,3 +60,3 @@ | ||
// Evolution doesn't seem to provide very consistent line endings | ||
var cal = icalendar.iCalendar.parse( | ||
var cal = parse_calendar( | ||
fs.readFileSync(__dirname+'/evolution.ics', 'utf8')); | ||
@@ -66,3 +66,3 @@ }); | ||
it('parsing', function() { | ||
var cal = icalendar.iCalendar.parse( | ||
var cal = parse_calendar( | ||
'BEGIN:VCALENDAR\r\n'+ | ||
@@ -92,3 +92,3 @@ 'PRODID:-//Bobs Software Emporium//NONSGML Bobs Calendar//EN\r\n'+ | ||
it('parse torture test', function() { | ||
var cal = icalendar.iCalendar.parse( | ||
var cal = parse_calendar( | ||
fs.readFileSync(__dirname+'/icalendar-test.ics', 'utf8')); | ||
@@ -115,3 +115,3 @@ | ||
it('uses timezone data parameter when parsing', function() { | ||
var cal = icalendar.parse_calendar( | ||
var cal = parse_calendar( | ||
'BEGIN:VCALENDAR\r\n'+ | ||
@@ -118,0 +118,0 @@ 'PRODID:-//Google Inc//Google Calendar 70.9054//EN\r\n'+ |
var RRule = require('../lib/icalendar').RRule; | ||
var RRule = require('../lib/rrule').RRule; | ||
@@ -4,0 +4,0 @@ describe("RRule", function() { |
var icalendar = require('../lib/icalendar'); | ||
var icalendar = require('../lib'); | ||
var assert = require('assert'); | ||
@@ -4,0 +4,0 @@ |
var icalendar = require('../lib/icalendar'); | ||
var icalendar = require('../lib'); | ||
@@ -4,0 +4,0 @@ describe('VEvent objects', function() { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
20
3
160106
1643
1