nowk/calendr.js

View on GitHub
recurrences/index.js

Summary

Maintainability
A
2 hrs
Test Coverage
/* jshint node: true */

var parseDate = require("../utils").parseDate;
var days = require("../utils").days.map(function(v) {
  return v.toLowerCase();
});

/**
 * expose
 */

module.exports = recurrences;

/**
 * dayofWeekIndex returns the index of the day of the week by name
 * week starts on Sunday
 *
 * @param {String} dayName
 * @return {Number}
 * @api private
 */

var dayofWeekIndex = function(dayName) {
  return days.indexOf(dayName.toLowerCase());
};

/**
 * condition is the conditional function for each recurrence type
 * TODO optimize
 */

var condition = {
  daily: function(d) {
    return d.valueOf() >= this.from();
  },
  weekly: function(d) {
    var daysInWeek = this.onDays().map(function(v) {
      return dayofWeekIndex(v);
    });
    return d.valueOf() >= this.from() && ~daysInWeek.indexOf(d.day());
  },
  monthly: function(d) {
    var m = parseDate(this.from());
    return d.date() === m.date();
  },
  yearly: function(d) {
    var m = parseDate(this.from());
    return d.month() === m.month() && d.date() === m.date();
  }
};

/**
 * recurrences returns copies of the event for the days in which they recurr
 *
 * @param {Event} event
 * @param {[Day]} range
 * @return {[Event]}
 * @api public
 */

function recurrences(event, range) {
  var recur = new Recur(event);
  if (!recur.yes()) {
    return [event];
  }

  var type = recur.type();
  var from = recur.from();
  var till = recur.till();

  var fn = condition[type];
  if (!!!fn) {
    return [];
  }
  
  var events = [];
  var i = 0;
  var len = range.length;
  var cond = fn.bind(recur);
  for(; i < len; i++) {
    var d = parseDate(range[i].moment, event.tz);
    // adjust to the events timezone for comparison

    var date = d.valueOf();
    if (till && date > till) {
      break;
    }

    if (cond(d)) {
      events.push(clone(event, d));
    }
  }

  return events;
}

/**
 * Recur
 *
 * @constructor
 * @api private
 */

function Recur(event) {
  this.event = event; 
}

/**
 * from returns the date from which the recurrence starts
 *
 * @param {Number} (date as int)
 * @api public
 */

Recur.prototype.from = function() {
  var s = parseDate(this.event.starts);
  s.startOf("day");
  return s.valueOf();
};

/**
 * typesMap maps repeats values to moment add/subtract keys
 */

var typesMap = {
  daily:   "days",
  weekly:  "weeks",
  monthly: "months",
  yearly:  "years"
};

/**
 * till returns teh date at which the recurrence stops
 *
 * @param {Number} (date as int)
 * @api public
 */

Recur.prototype.till = function() {
  var ends = parseDate(this.event.repeatEndsOn);
  if (!!ends) {
    ends.startOf("day");
    return ends.valueOf();
  }

  var type = this.type();
  var n = this.event.repeatTimes;
  var intv = typesMap[type];
  if (n > 0) {
    var s = parseDate(this.event.starts);
    s.startOf("day");
    if ("weekly" === type) {
      s.startOf("week");
    }
    s.add(n, intv);
    return s.valueOf();
  }
};

/**
 * type returns the repeats types, eg. daily, weekly, etc...
 *
 * @return {String}
 * @api public
 */

Recur.prototype.type = function() {
  return this.event.repeats;
};

/**
 * onDays returns the days in which an event recurs
 * Only available for weekly recurrences
 *
 * @return {[String]}
 * @api public
 */

Recur.prototype.onDays = function() {
  return this.event.repeatsOn;
};

/**
 * yes returns true if event repeats
 *
 * @return {Bool}
 * @api public
 */

Recur.prototype.yes = function() {
  return !!this.type();
};

/**
 * tz returns the timezone based on the event starts time
 *
 * @return {Number} timezone offset
 * @api public
 */

Recur.prototype.tz = function() {
  return this.event.starts.zone();
};

/**
 * Event provides a clnne of an event matched to the date of the recurrence
 *
 * @param {Event} event
 * @param {Moment} date
 * @constructor
 * @api private
 */

function Event(event, date) {
  this.event = event; 

  var keys = Object.keys(this.event);
  var self = this;
  var i = 0;
  var len = keys.length;
  for(; i < len; i++) {
    var k = keys[i];
    Object.defineProperty(self, k, {
      value: event[k],
      enumerable: true,
      writable: true
    });
  }

  var n = recurOffset(event.starts, date);
  var tz = this.tz;

  this.starts = parseDate(event.starts.valueOf() + n, tz);
  this.ends = parseDate(event.ends.valueOf() + n, tz);
}

/**
 * recurOffset returns the offset value of the original event from the current
 *
 * @param {Moment} a
 * @param {Moment} b
 * @return {Number}
 * @api private
 */

function recurOffset(a, b) {
  var n = parseDate(a);
  n.startOf("day");
  var m = parseDate(b);
  m.startOf("day");

  return m.valueOf() - n.valueOf();
}

/**
 * clone
 *
 * @param {Event} event
 * @param {Moment} date
 * @return {Event}
 * @api private
 */

function clone(event, date) {
  Event.prototype = event;
  return new Event(event, date);
}