JSCron よりもうちょっと Cron っぽいことをする

JSCron が Cron としてはかなりいまいちで不満だったので、適当にでっちあげてみた。

if (!Array.prototype.indexOf) {
  Array.prototype.indexOf = function (elt /* , from */) {
    var len = this.length;
    var from = Number(arguments[1]) || 0;
    from = (from < 0) ? Math.ceil(from) : Math.floor(from);
    if (from < 0) from += len;
    for (;from < len; from++) {
      if (from in this && this[from] === elt) return from;
    }
    return -1;
  }
}

function Cron() {
  var self = arguments.callee;
  if (self.instance == null) {
    this.initialize.apply(this, arguments);
    self.instance = this;
  }
  return self.instance;
}

Cron.prototype.month_mapping = [
  'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'
];
Cron.prototype.week_mapping = [
  'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
];

Cron.prototype.REGEXP_NUMBER = '((?:\\d|\\*)(?:[\\d\\*\\,\\-\\/]*\\d)?)';
Cron.prototype.REGEXP_MONTH = '((?:\\d|\\*)(?:[\\d\\*\\,\\-\\/]*\\d)?|' 
    + Cron.prototype.month_mapping.join('|') +')';
Cron.prototype.REGEXP_WEEKDAY = '((?:\\d|\\*)(?:[\\d\\*\\,\\-\\/]*\\d)?|'
    + Cron.prototype.week_mapping.join('|') + ')';
Cron.prototype.REGEXP_CRON = new RegExp(
  '^' + [
    Cron.prototype.REGEXP_NUMBER,
    Cron.prototype.REGEXP_NUMBER,
    Cron.prototype.REGEXP_NUMBER,
    Cron.prototype.REGEXP_NUMBER,
    Cron.prototype.REGEXP_MONTH,
    Cron.prototype.REGEXP_WEEKDAY
  ].join(' ') + '$', 'i'
);

Cron.prototype.initialize = function() {
  this.tasks = {};
  this.counter = 0;
  return this;
}

Cron.prototype.wakeUp = function() {
  var self = this;
  this.pid = setInterval(function() {
    self.exec.apply(self);
  },1000);
}
Cron.prototype.shutdown = function() {
  clearInterval(this.pid);
}

Cron.prototype.parse = function(cronfield) {
  function expandAsterisk(max) {
    var expando = [];
    for(var i = 0; i < max; i++) expando.push(i);
    return expando;
  }

  function expandInterval(spec, max) {
    var expando = [];
    var sp = spec.split('/');
    if (sp.length != 2) throw "illigal spec: " + spec;

    var interval = parseInt(sp[1]);
    if (interval <= 0) throw "illigal spec: " + spec;
    var range;
    if (sp[0].charAt(0) == '*') {
      range = [0, max - 1];
    } else {
      range = sp[0].split('-');
      range = [parseInt(range[0]), parseInt(range[1])];
    }
    if (range.length != 2 || range[0] > range[1]) throw "illigal spec: " + spec; 

    for (var pos = range[0], max = range[1]; pos <= max; pos += interval) expando.push(pos);
    return expando;
  }

  function expandRange(spec, max) {
    var expando = [];
    var range = spec.split('-');
    range = [parseInt(range[0]), parseInt(range[1])];

    if (range[0] < 0 || range[1] > max) throw "illigal spec: " + spec;

    for (var i = range[0], rangeMax = range[1]; i <= rangeMax; i++) expando.push(i);
    return expando;
  }

  function expand(spec, max) {
    var expando = [];
    var intValue = parseInt(spec);
    if (typeof spec == 'number') {
      expando.push(spec);
    } else if (spec.indexOf(',') != -1) {
      var specs = spec.split(',');
      for (var i = 0, length = specs.length; i < length; i++)
        expando = expando.concat(expand(specs[i], max));
    } else if (spec.indexOf('/') != -1) {
      expando = expando.concat(expandInterval(spec, max));
    } else if (spec.indexOf('-') != -1) {
      expando = expando.concat(expandRange(spec, max));
    } else if (spec == '*') {
      expando = expando.concat(expandAsterisk(max));
    } else if (intValue >= 0 && intValue <= max) {
      expando.push(intValue);
    } else {
      throw "illigal spec: " + spec;
    }
    return expando;
  }

  var timespec = this.REGEXP_CRON.exec(cronfield);
  if (!timespec) throw 'illigal arguments: ' + cronfield;
  timespec.shift();
  var month = this.month_mapping.indexOf(timespec[4].toLowerCase());
  var weekday = this.week_mapping.indexOf(timespec[5].toLowerCase());
  if (month != -1) timespec[4] = month;
  if (weekday != -1) timespec[5] = weekday;
  if (timespec[5] == 7) timespec[5] = 0;
  return [
    expand(timespec[0], 60), // seconds
    expand(timespec[1], 60), // minutes
    expand(timespec[2], 24), // hours
    expand(timespec[3], 31), // day
    expand(timespec[4], 12), // month
    expand(timespec[5], 7) // weekday
  ];
}
Cron.prototype.register = function (fn, spec) {
  var id = this.counter;
  if (id == 0) this.wakeUp();
  var sp = this.parse(spec);
  this.tasks['_' + id] = {
    callback: fn,
    timespec: sp
  };
  this.counter++;
  return id;
}
Cron.prototype.exec = function () {
  var now = new Date();
  var currents = [now.getSeconds(), now.getMinutes(), now.getHours(), now.getDate(), now.getMonth(), now.getDay()];
  for(var i in this.tasks) if (this.tasks.hasOwnProperty(i)) {
    var spec = this.tasks[i];
    var matched = true;
    for (var l = 0, len = currents.length; l < len; l++) {
      if (spec.timespec[l].indexOf(currents[l]) == -1) {
        matched = false;
        break;
      }
    }
    if (matched) spec.callback.apply();
  }
  return this;
}
Cron.prototype.cancel = function(id) {
  delete this.tasks['_' + id];
  for (var i in this.tasks) if (this.tasks.hasOwnProperty(i)) return this;
  this.shutdown();
  return this;
}

/**
console.log(new Date());
var timer = new Cron();
var pid = timer.register(function() {
  console.log(new Date());
}, "1,3,10-15,20-59/5 0-33/1,35-37,40-59 * * JUL mon");

setTimeout(function() {
  console.log('cancel: ' + pid);
  timer.cancel(pid);
},10000);
*/

http://github.com/send/misc/tree/master の js/cron.js に置いてありますが、 gist にしなかったのはバカだなーと思いますが、コミットしたら面倒になってしまったので gist や coderepos 等に持っていきたい人は好きに持っていって弄って下さい。
バグは勿論、パフォーマンス面やメモリの使いかた等、不味いところはまだまだあるのはわかってるので、なんかあったら教えて下さると俺が喜びます。