// TAG index 「JavaScript で Decimal 等」スレ
// http://www.tagindex.com/cgi-lib/bbs/patio.cgi?mode=view&no=339

////////////////////////////////////////////////////////////////////////
// レス#9
// http://www.tagindex.com/cgi-lib/bbs/patio.cgi?mode=view2&f=339&no=9
////////////////////////////////////////////////////////////////////////

function DateTime () { ; }


DateTime.prototype.toString = function () {
  return '[object DateTime]';
};


DateTime.prototype.toUTCDateTime = function () {
  if (this.timeZoneOffset) {
    var d = addDurationToDateTime (this, Duration.create (null, null, null, null, -(this.timeZoneOffset), null));
    d.timeZoneOffset = 0;
    return d;
  }
  return this;
};


DateTime.prototype.replaceTimeZoneOffset = function (timeZoneOffset) {
  return DateTime.create (this.year, this.month, this.date, this.hours, this.minutes, this.seconds, timeZoneOffset);
};


DateTime.create = function (year, month, date, hours, minutes, seconds, timeZoneOffset) {
  var d = new this;
  if ('number' === typeof year)    d.year    = year;
  if ('number' === typeof month)   d.month   = month;
  if ('number' === typeof date)    d.date    = date;
  if ('number' === typeof hours)   d.hours   = hours;
  if ('number' === typeof minutes) d.minutes = minutes;
  if ('number' === typeof seconds) d.seconds = seconds;
  if ('number' === typeof timeZoneOffset) d.timeZoneOffset = timeZoneOffset;
  return d;
};


DateTime.fromStrings = function (year, month, date, hours, minutes, seconds, timeZoneOffset) {
  return DateTime.create.call (this,
    year    ? parseInt (year   , 10) : null,
    month   ? parseInt (month  , 10) : null,
    date    ? parseInt (date   , 10) : null,
    hours   ? parseInt (hours  , 10) : null,
    minutes ? parseInt (minutes, 10) : null,
    seconds ? parseFloat (seconds) : null,
    timeZoneOffset ? parseInt (timeZoneOffset, 10) : null
  );
};


DateTime.createDateFromStrings = function (year, month, date, hours, minutes, seconds, timeZoneOffset) {
  var d = new Date;
  var s = seconds ? seconds.split ('.') : [ ];
  
  return new Date (
    year  ? parseInt (year , 10) : d.getFullYear (),
    month ? parseInt (month, 10) - 1 : (year ? 0 : d.getMonth ()),
    date  ? parseInt (date , 10) : (year ? 1 : d.getDate ()),
    hours ? parseInt (hours, 10) : 0,
    (minutes ? parseInt (minutes, 10) : 0) - (timeZoneOffset || 0) - d.getTimezoneOffset (),
    s[0] ? parseInt (s[0], 10) : 0,
    s[1] ? parseInt (s[1], 10) : 0
  );
};


function isDateTime (arg) {
  return arg instanceof DateTime;
}


//
function getTimezoneOffset (sign, hours, minutes) {
  if (! sign && ! hours && ! minutes) {
    return null;
  }
  var m = 0;
  
  if (sign === 'Z') {
    return String (m);
  }
  if (hours)   m += parseInt (sign + hours  , 10) * 60;
  if (minutes) m += parseInt (sign + minutes, 10);
  return String (m);
}

////////////////////////////////////////////////////////////////////////
// レス#10
// 2000-01-12T12:13:14Z + P1Y3M5DT7H10M3.3S => 2001-04-17T19:23:17.3Z
////////////////////////////////////////////////////////////////////////

var addDurationToDateTime = (function () {
  return function (dateTime, duration) {
    var end = { };
    var result = DateTime.create ();
    var carried;
    var tmp;
    
    tmp = (dateTime.month || 1) + (duration.months || 0);
    end.month = moduloB (tmp, 1, 13);
    carried = quotientB (tmp, 1, 13);
    
    tmp = (dateTime.year || 1) + (duration.years || 0) + carried;
    end.year = tmp;
    
    tmp = (dateTime.timeZoneOffset || 0);
    end.timeZoneOffset = tmp;
    
    tmp = (dateTime.seconds || 0) + (duration.seconds || 0);
    end.seconds = moduloA (tmp, 60);
    carried = quotientA (tmp, 60);
    
    tmp = (dateTime.minutes || 0) + (duration.minutes || 0) + carried;
    end.minutes = moduloA (tmp, 60);
    carried = quotientA (tmp, 60);
    
    tmp = (dateTime.hours || 0) + (duration.hours || 0) + carried;
    end.hours = moduloA (tmp, 24);
    carried = quotientA (tmp, 24);
    
    tmp = getMonthLastDate (end.year, end.month);
    tmp = dateTime.date ? (dateTime.date < 1) ? 1 : (tmp < dateTime.date) ? tmp : dateTime.date : 1;
    end.date = tmp + (duration.days || 0) + carried;
    
    while (true) {
      if (end.date < 1) {
        end.date += getMonthLastDate (end.year, end.month - 1);
        carried = -1;
      } else {
        tmp = getMonthLastDate (end.year, end.month);
        
        if (end.date > tmp) {
          end.date -= tmp;
          carried = 1;
        } else {
          break;
        }
      }
      tmp = end.month + carried;
      end.month = moduloB (tmp, 1, 13);
      end.year += quotientB (tmp, 1, 13);
    }
    for (tmp in dateTime) {
      if (dateTime.hasOwnProperty (tmp)) {
        result[tmp] = end[tmp];
      }
    }
    return result;
  };
  
  function quotientA (a, b) {
    return Math.floor (a / b);
  }
  
  function moduloA (a, b) {
    return a - quotientA (a, b) * b;
  }
  
  function quotientB (a, low, high) {
    return quotientA (a - low, high - low);
  }
  
  function moduloB (a, low, high) {
    return moduloA (a - low, high - low) + low;
  }
  
  function getMonthLastDate (year, month) {
    // return new Date (year, month, 0).getDate ();
    
    var m = moduloB (month, 1, 13);
    var y = year + quotientB (month, 1, 13);
    
    switch (m) {
    case 1 : case 3 : case 5 : case 7 : case 8 : case 10 : case 12 : return 31;
    case 4 : case 6 : case 9 : case 11 : return 30;
    case 2 : return 28 + Boolean (moduloA (Y, 400) === 0 || moduloA (Y, 100) !== 0 && moduloA (Y, 4) === 0);
    default : throw new Error;
    }
  }
})();


// dateTime1 < dateTime2 => -1
// dateTime1 > dateTime2 =>  1
// dateTime1 = dateTime2 =>  0
// dateTime1 <> dateTime2 => null (may not be allowed in Array#sort)
var compareDateTimes = (function () {
  var P = [ 'year', 'month', 'date', 'hours', 'minutes', 'seconds' ];
  var Plength = P.length;
  
  return function (dateTime1, dateTime2) {
    var d1 = dateTime1.toUTCDateTime ();
    var d2 = dateTime2.toUTCDateTime ();
    var b1 = ('number' === typeof d1.timeZoneOffset);
    var b2 = ('number' === typeof d2.timeZoneOffset);
    var i;
    var x;
    
    if (b1 && ! b2) {
      x = arguments.callee (d1, d2.replaceTimeZoneOffset (14 * 60));
      
      if (x < 0) {
        return x;
      }
      x = arguments.callee (d1, s2.replaceTimeZoneOffset (-14 * 60));
      
      if (x > 0) {
        return x;
      }
      return null;
    }
    if (! b1 && b2) {
      x = arguments.callee (d1.replaceTimeZoneOffset (-14 * 60), d2);
      
      if (x < 0) {
        return x;
      }
      x = arguments.callee (d1.replaceTimeZoneOffset (14 * 60), d2);
      
      if (x > 0) {
        return x;
      }
      return null;
    }
    for (i = 0; i < Plength; i++) {
      var v1 = d1[P[i]];
      var v2 = d2[P[i]];
      var w1 = ('number' === typeof v1);
      var w2 = ('number' === typeof v2);
      
      if (! w1 && ! w2) {
        continue;
      }
      if (w1 && w2) {
        x = (v1 > v2) - (v1 < v2);
        
        if (x !== 0) {
          return x;
        }
        continue;
      }
      return null;
    }
    return 0;
  };
})();

// http://www.tagindex.com/cgi-lib/bbs/patio.cgi?mode=view2&f=339&no=10

function XDate () { ; }
XDate.prototype = new DateTime;
XDate.prototype.constructor = XDate;
function isXDate (arg) { return arg instanceof isXDate; }


function XTime () { ; }
XTime.prototype = new DateTime;
XTime.prototype.constructor = XTime;
function isXTime (arg) { return arg instanceof isXTime; }


function GYearMonth () { ; }
GYearMonth.prototype = new DateTime;
GYearMonth.prototype.constructor = GYearMonth;
function isGYearMonth (arg) { return arg instanceof isGYearMonth; }


function GMonthDay () { ; }
GMonthDay.prototype = new DateTime;
GMonthDay.prototype.constructor = GMonthDay;
function isGMonthDay (arg) { return arg instanceof isGMonthDay; }


function GYear () { ; }
GYear.prototype = new DateTime;
GYear.prototype.constructor = GYear;
function isGYear (arg) { return arg instanceof isGYear; }


function GMonth () { ; }
GMonth.prototype = new DateTime;
GMonth.prototype.constructor = GMonth;
function isGMonth (arg) { return arg instanceof isGMonth; }


function GDay () { ; }
GDay.prototype = new DateTime;
GDay.prototype.constructor = GDay;
function isGDay (arg) { return arg instanceof isGDay; }


//
var DateTimeTokenizer = new function () {
  var yearFrag   = '(-?(?:[1-9][0-9][0-9][0-9]+|0[0-9][0-9][0-9]))';
  var monthFrag  = '(0[1-9]|1[0-2])';
  var dayFrag    = '(0[1-9]|[12][0-9]|3[01])';
  var hourFrag   = '([01][0-9]|2[0-3])';
  var minuteFrag = '([0-5][0-9])';
  var secondFrag = '([0-5][0-9](?:\\.[0-9]+)?)';
  var endOfDayFrag = '(24):(00):(00(?:\\.0+)?)';
  var timezoneFrag = '(Z)|([-+])(?:(0[0-9]|1[0-3]):' + minuteFrag + '|(14):(00))';
  
  var dateLexicalFrag = yearFrag + '-' + monthFrag + '-' + dayFrag;
  var timeLexicalFrag = hourFrag + ':' + minuteFrag + ':' + secondFrag + '|' + endOfDayFrag;
  
  var dateLexicalRep = dateLexicalFrag + '(?:' + timezoneFrag + ')?';
  var timeLexicalRep = '(?:' + timeLexicalFrag + ')(?:' + timezoneFrag + ')?';
  var dateTimeLexicalRep = dateLexicalFrag + 'T(?:' + timeLexicalFrag + ')(?:' + timezoneFrag + ')?';
  
  var gYearMonthLexicalRep = yearFrag + '-' + monthFrag + '(?:' + timezoneFrag + ')?';
  var gYearLexicalRep    = yearFrag + '(?:' + timezoneFrag + ')?';
  var gMonthDayLexicalRep  = '--' + monthFrag + '-' + dayFrag + '(?:' + timezoneFrag + ')?';
  var gMonthLexicalRep   = '--' + monthFrag + '(?:' + timezoneFrag + ')?';
  var gDayLexicalRep     = '---' + dayFrag + '(?:' + timezoneFrag + ')?';
  
  this.DateTime = dateTimeLexicalRep;
  this.Date = dateLexicalRep;
  this.Time = timeLexicalRep;
  
  this.GYearMonth = gYearMonthLexicalRep;
  this.GMonthDay  = gMonthDayLexicalRep;
  
  this.GYear  = gYearLexicalRep;
  this.GMonth = gMonthLexicalRep;
  this.GDay   = gDayLexicalRep;
};


// 2009-11-16T12:00:00+09:00
var createDateTimeComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.DateTime + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, r[1], r[2], r[3], r[4] || r[7], r[5] || r[8], r[6] || r[9], getTimezoneOffset (r[10] || r[11], r[12] || r[14], r[13] || r[15]));
      }
      throw new Error ('createDateTimeComposer');
    };
  };
})(DateTimeTokenizer);

var parseDateTime = createDateTimeComposer (DateTime.fromStrings, DateTime);
var parseDateTimeToDate = createDateTimeComposer (DateTime.createDateFromStrings, DateTime);


// 2009-11-15+09:00
var createXDateComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.Date + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, r[1], r[2], r[3], null, null, null, getTimezoneOffset (r[4] || r[5], r[6] || r[8], r[7] || r[9]));
      }
      throw new Error ('createXDateComposer');
    };
  };
})(DateTimeTokenizer);

var parseXDate = createXDateComposer (DateTime.fromStrings, XDate);
var parseXDateToDate = createXDateComposer (DateTime.createDateFromStrings, XDate);


// 12:59:59.9999+09:00
var createXTimeComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.Time + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, null, null, null, r[1] || r[4], r[2] || r[5], r[3] || r[6], getTimezoneOffset (r[7] || r[8], r[9] || r[11], r[10] || r[12]));
      }
      throw new Error ('createXTimeComposer');
    };
  };
})(DateTimeTokenizer);

var parseXTime = createXTimeComposer (DateTime.fromStrings, XTime);
var parseXTimeToDate = createXTimeComposer (DateTime.createDateFromStrings, XTime);


// 2002-12+11:00
var createGYearMonthComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.GYearMonth + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, r[1], r[2], null, null, null, null, getTimezoneOffset (r[3] || r[4], r[5] || r[7], r[6] || r[8]));
      }
      throw new Error ('createGYearMonthComposer');
    };
  };
})(DateTimeTokenizer);

var parseGYearMonth = createGYearMonthComposer (DateTime.fromStrings, GYearMonth);
var parseGYearMonthToDate = createGYearMonthComposer (DateTime.createDateFromStrings, GYearMonth);


// --12-12+11:00
var createGMonthDayComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.GMonthDay + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, null, r[1], r[2], null, null, null, getTimezoneOffset (r[3] || r[4], r[5] || r[7], r[6] || r[8]));
      }
      throw new Error ('createGMonthDayComposer');
    };
  };
})(DateTimeTokenizer);

var parseGMonthDay = createGMonthDayComposer (DateTime.fromStrings, GMonthDay);
var parseGMonthDayToDate = createGMonthDayComposer (DateTime.createDateFromStrings, GMonthDay);


// 2009+11:00
var createGYearComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.GYear + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, r[1], null, null, null, null, null, getTimezoneOffset (r[2] || r[3], r[4] || r[6], r[5] || r[7]));
      }
      throw new Error ('createGYearComposer');
    };
  };
})(DateTimeTokenizer);

var parseGYear = createGYearComposer (DateTime.fromStrings, GYear);
var parseGYearToDate = createGYearComposer (DateTime.createDateFromStrings, GYear);


// --12+12:00
var createGMonthComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.GMonth + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, null, r[1], null, null, null, null, getTimezoneOffset (r[2] || r[3], r[4] || r[6], r[5] || r[7]));
      }
      throw new Error ('createGMonthComposer');
    };
  };
})(DateTimeTokenizer);

var parseGMonth = createGMonthComposer (DateTime.fromStrings, GMonth);
var parseGMonthToDate = createGMonthComposer (DateTime.createDateFromStrings, GMonth);


// ---01+13:00
var createGDayComposer = (function (dt) {
  var P = new RegExp ('^(?:' + dt.GDay + ')$');
  
  return function (composer, thisArg) {
    return function (stringData) {
      var r;
      
      if (r = P.exec (stringData)) {
        return composer.call (thisArg, null, null, r[1], null, null, null, getTimezoneOffset (r[2] || r[3], r[4] || r[6], r[5] || r[7]));
      }
      throw new Error ('createGDayComposer');
    };
  };
})(DateTimeTokenizer);

var parseGDay = createGDayComposer (DateTime.fromStrings, GDay);
var parseGDayToDate = createGDayComposer (DateTime.createDateFromStrings, GDay);

