date.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  1. /*
  2. * Utilities: A classic collection of JavaScript utilities
  3. * Copyright 2112 Matthew Eernisse ([email protected])
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. */
  18. var string = require('./string')
  19. , date
  20. , log = require('./log');
  21. /**
  22. @name date
  23. @namespace date
  24. */
  25. date = new (function () {
  26. var _this = this
  27. , _date = new Date();
  28. var _US_DATE_PAT = /^(\d{1,2})(?:\-|\/|\.)(\d{1,2})(?:\-|\/|\.)(\d{4})/;
  29. var _DATETIME_PAT = /^(\d{4})(?:\-|\/|\.)(\d{1,2})(?:\-|\/|\.)(\d{1,2})(?:T| )?(\d{2})?(?::)?(\d{2})?(?::)?(\d{2})?(?:\.)?(\d+)?(?: *)?(Z|[+-]\d{4}|[+-]\d{2}:\d{2}|[+-]\d{2})?/;
  30. // TODO Add am/pm parsing instead of dumb, 24-hour clock.
  31. var _TIME_PAT = /^(\d{1,2})?(?::)?(\d{2})?(?::)?(\d{2})?(?:\.)?(\d+)?$/;
  32. var _dateMethods = [
  33. 'FullYear'
  34. , 'Month'
  35. , 'Date'
  36. , 'Hours'
  37. , 'Minutes'
  38. , 'Seconds'
  39. , 'Milliseconds'
  40. ];
  41. var _isArray = function (obj) {
  42. return obj &&
  43. typeof obj === 'object' &&
  44. typeof obj.length === 'number' &&
  45. typeof obj.splice === 'function' &&
  46. !(obj.propertyIsEnumerable('length'));
  47. };
  48. this.weekdayLong = ['Sunday', 'Monday', 'Tuesday',
  49. 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  50. this.weekdayShort = ['Sun', 'Mon', 'Tue', 'Wed',
  51. 'Thu', 'Fri', 'Sat'];
  52. this.monthLong = ['January', 'February', 'March',
  53. 'April', 'May', 'June', 'July', 'August', 'September',
  54. 'October', 'November', 'December'];
  55. this.monthShort = ['Jan', 'Feb', 'Mar', 'Apr',
  56. 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  57. this.meridiem = {
  58. 'AM': 'AM',
  59. 'PM': 'PM'
  60. }
  61. // compat
  62. this.meridian = this.meridiem
  63. /**
  64. @name date#supportedFormats
  65. @public
  66. @object
  67. @description List of supported strftime formats
  68. */
  69. this.supportedFormats = {
  70. // abbreviated weekday name according to the current locale
  71. 'a': function (dt) { return _this.weekdayShort[dt.getDay()]; },
  72. // full weekday name according to the current locale
  73. 'A': function (dt) { return _this.weekdayLong[dt.getDay()]; },
  74. // abbreviated month name according to the current locale
  75. 'b': function (dt) { return _this.monthShort[dt.getMonth()]; },
  76. 'h': function (dt) { return _this.strftime(dt, '%b'); },
  77. // full month name according to the current locale
  78. 'B': function (dt) { return _this.monthLong[dt.getMonth()]; },
  79. // preferred date and time representation for the current locale
  80. 'c': function (dt) { return _this.strftime(dt, '%a %b %d %T %Y'); },
  81. // century number (the year divided by 100 and truncated
  82. // to an integer, range 00 to 99)
  83. 'C': function (dt) { return _this.calcCentury(dt.getFullYear());; },
  84. // day of the month as a decimal number (range 01 to 31)
  85. 'd': function (dt) { return string.lpad(dt.getDate(), '0', 2); },
  86. // same as %m/%d/%y
  87. 'D': function (dt) { return _this.strftime(dt, '%m/%d/%y') },
  88. // day of the month as a decimal number, a single digit is
  89. // preceded by a space (range ' 1' to '31')
  90. 'e': function (dt) { return string.lpad(dt.getDate(), ' ', 2); },
  91. // month as a decimal number, a single digit is
  92. // preceded by a space (range ' 1' to '12')
  93. 'f': function () { return _this.strftimeNotImplemented('f'); },
  94. // same as %Y-%m-%d
  95. 'F': function (dt) { return _this.strftime(dt, '%Y-%m-%d'); },
  96. // like %G, but without the century.
  97. 'g': function () { return _this.strftimeNotImplemented('g'); },
  98. // The 4-digit year corresponding to the ISO week number
  99. // (see %V). This has the same format and value as %Y,
  100. // except that if the ISO week number belongs to the
  101. // previous or next year, that year is used instead.
  102. 'G': function () { return _this.strftimeNotImplemented('G'); },
  103. // hour as a decimal number using a 24-hour clock (range
  104. // 00 to 23)
  105. 'H': function (dt) { return string.lpad(dt.getHours(), '0', 2); },
  106. // hour as a decimal number using a 12-hour clock (range
  107. // 01 to 12)
  108. 'I': function (dt) { return string.lpad(
  109. _this.hrMil2Std(dt.getHours()), '0', 2); },
  110. // day of the year as a decimal number (range 001 to 366)
  111. 'j': function (dt) { return string.lpad(
  112. _this.calcDays(dt), '0', 3); },
  113. // Hour as a decimal number using a 24-hour clock (range
  114. // 0 to 23 (space-padded))
  115. 'k': function (dt) { return string.lpad(dt.getHours(), ' ', 2); },
  116. // Hour as a decimal number using a 12-hour clock (range
  117. // 1 to 12 (space-padded))
  118. 'l': function (dt) { return string.lpad(
  119. _this.hrMil2Std(dt.getHours()), ' ', 2); },
  120. // month as a decimal number (range 01 to 12)
  121. 'm': function (dt) { return string.lpad((dt.getMonth()+1), '0', 2); },
  122. // minute as a decimal number
  123. 'M': function (dt) { return string.lpad(dt.getMinutes(), '0', 2); },
  124. // Linebreak
  125. 'n': function () { return '\n'; },
  126. // either `am' or `pm' according to the given time value,
  127. // or the corresponding strings for the current locale
  128. 'p': function (dt) { return _this.getMeridian(dt.getHours()); },
  129. // time in a.m. and p.m. notation
  130. 'r': function (dt) { return _this.strftime(dt, '%I:%M:%S %p'); },
  131. // time in 24 hour notation
  132. 'R': function (dt) { return _this.strftime(dt, '%H:%M'); },
  133. // second as a decimal number
  134. 'S': function (dt) { return string.lpad(dt.getSeconds(), '0', 2); },
  135. // Tab char
  136. 't': function () { return '\t'; },
  137. // current time, equal to %H:%M:%S
  138. 'T': function (dt) { return _this.strftime(dt, '%H:%M:%S'); },
  139. // weekday as a decimal number [1,7], with 1 representing
  140. // Monday
  141. 'u': function (dt) { return _this.convertOneBase(dt.getDay()); },
  142. // week number of the current year as a decimal number,
  143. // starting with the first Sunday as the first day of the
  144. // first week
  145. 'U': function () { return _this.strftimeNotImplemented('U'); },
  146. // week number of the year (Monday as the first day of the
  147. // week) as a decimal number [01,53]. If the week containing
  148. // 1 January has four or more days in the new year, then it
  149. // is considered week 1. Otherwise, it is the last week of
  150. // the previous year, and the next week is week 1.
  151. 'V': function () { return _this.strftimeNotImplemented('V'); },
  152. // week number of the current year as a decimal number,
  153. // starting with the first Monday as the first day of the
  154. // first week
  155. 'W': function () { return _this.strftimeNotImplemented('W'); },
  156. // day of the week as a decimal, Sunday being 0
  157. 'w': function (dt) { return dt.getDay(); },
  158. // preferred date representation for the current locale
  159. // without the time
  160. 'x': function (dt) { return _this.strftime(dt, '%D'); },
  161. // preferred time representation for the current locale
  162. // without the date
  163. 'X': function (dt) { return _this.strftime(dt, '%T'); },
  164. // year as a decimal number without a century (range 00 to
  165. // 99)
  166. 'y': function (dt) { return _this.getTwoDigitYear(dt.getFullYear()); },
  167. // year as a decimal number including the century
  168. 'Y': function (dt) { return string.lpad(dt.getFullYear(), '0', 4); },
  169. // time zone or name or abbreviation
  170. 'z': function () { return _this.strftimeNotImplemented('z'); },
  171. 'Z': function () { return _this.strftimeNotImplemented('Z'); },
  172. // Literal percent char
  173. '%': function (dt) { return '%'; }
  174. };
  175. /**
  176. @name date#getSupportedFormats
  177. @public
  178. @function
  179. @description return the list of formats in a string
  180. @return {String} The list of supported formats
  181. */
  182. this.getSupportedFormats = function () {
  183. var str = '';
  184. for (var i in this.supportedFormats) { str += i; }
  185. return str;
  186. }
  187. this.supportedFormatsPat = new RegExp('%[' +
  188. this.getSupportedFormats() + ']{1}', 'g');
  189. /**
  190. @name date#strftime
  191. @public
  192. @function
  193. @return {String} The `dt` formated with the given `format`
  194. @description Formats the given date with the strftime formated
  195. @param {Date} dt the date object to format
  196. @param {String} format the format to convert the date to
  197. */
  198. this.strftime = function (dt, format) {
  199. if (!dt) { return '' }
  200. var d = dt;
  201. var pats = [];
  202. var dts = [];
  203. var str = format;
  204. var key;
  205. // Allow either Date obj or UTC stamp
  206. d = typeof dt == 'number' ? new Date(dt) : dt;
  207. // Grab all instances of expected formats into array
  208. while (pats = this.supportedFormatsPat.exec(format)) {
  209. dts.push(pats[0]);
  210. }
  211. // Process any hits
  212. for (var i = 0; i < dts.length; i++) {
  213. key = dts[i].replace(/%/, '');
  214. str = str.replace('%' + key,
  215. this.supportedFormats[key](d));
  216. }
  217. return str;
  218. };
  219. this.strftimeNotImplemented = function (s) {
  220. throw('this.strftime format "' + s + '" not implemented.');
  221. };
  222. /**
  223. @name date#calcCentury
  224. @public
  225. @function
  226. @return {String} The century for the given date
  227. @description Find the century for the given `year`
  228. @param {Number} year The year to find the century for
  229. */
  230. this.calcCentury = function (year) {
  231. if(!year) {
  232. year = _date.getFullYear();
  233. }
  234. var ret = parseInt((year / 100) + 1);
  235. year = year.toString();
  236. // If year ends in 00 subtract one, because it's still the century before the one
  237. // it divides to
  238. if (year.substring(year.length - 2) === '00') {
  239. ret--;
  240. }
  241. return ret.toString();
  242. };
  243. /**
  244. @name date#calcDays
  245. @public
  246. @function
  247. @return {Number} The number of days so far for the given date
  248. @description Calculate the day number in the year a particular date is on
  249. @param {Date} dt The date to use
  250. */
  251. this.calcDays = function (dt) {
  252. var first = new Date(dt.getFullYear(), 0, 1);
  253. var diff = 0;
  254. var ret = 0;
  255. first = first.getTime();
  256. diff = (dt.getTime() - first);
  257. ret = parseInt(((((diff/1000)/60)/60)/24))+1;
  258. return ret;
  259. };
  260. /**
  261. * Adjust from 0-6 base week to 1-7 base week
  262. * @param d integer for day of week
  263. * @return Integer day number for 1-7 base week
  264. */
  265. this.convertOneBase = function (d) {
  266. return d == 0 ? 7 : d;
  267. };
  268. this.getTwoDigitYear = function (yr) {
  269. // Add a millenium to take care of years before the year 1000,
  270. // (e.g, the year 7) since we're only taking the last two digits
  271. // If we overshoot, it doesn't matter
  272. var millenYear = yr + 1000;
  273. var str = millenYear.toString();
  274. str = str.substr(2); // Get the last two digits
  275. return str
  276. };
  277. /**
  278. @name date#getMeridiem
  279. @public
  280. @function
  281. @return {String} Return 'AM' or 'PM' based on hour in 24-hour format
  282. @description Return 'AM' or 'PM' based on hour in 24-hour format
  283. @param {Number} h The hour to check
  284. */
  285. this.getMeridiem = function (h) {
  286. return h > 11 ? this.meridiem.PM :
  287. this.meridiem.AM;
  288. };
  289. // Compat
  290. this.getMeridian = this.getMeridiem;
  291. /**
  292. @name date#hrMil2Std
  293. @public
  294. @function
  295. @return {String} Return a 12 hour version of the given time
  296. @description Convert a 24-hour formatted hour to 12-hour format
  297. @param {String} hour The hour to convert
  298. */
  299. this.hrMil2Std = function (hour) {
  300. var h = typeof hour == 'number' ? hour : parseInt(hour);
  301. var str = h > 12 ? h - 12 : h;
  302. str = str == 0 ? 12 : str;
  303. return str;
  304. };
  305. /**
  306. @name date#hrStd2Mil
  307. @public
  308. @function
  309. @return {String} Return a 24 hour version of the given time
  310. @description Convert a 12-hour formatted hour with meridian flag to 24-hour format
  311. @param {String} hour The hour to convert
  312. @param {Boolean} pm hour is PM then this should be true
  313. */
  314. this.hrStd2Mil = function (hour, pm) {
  315. var h = typeof hour == 'number' ? hour : parseInt(hour);
  316. var str = '';
  317. // PM
  318. if (pm) {
  319. str = h < 12 ? (h+12) : h;
  320. }
  321. // AM
  322. else {
  323. str = h == 12 ? 0 : h;
  324. }
  325. return str;
  326. };
  327. // Constants for use in this.add
  328. var dateParts = {
  329. YEAR: 'year'
  330. , MONTH: 'month'
  331. , DAY: 'day'
  332. , HOUR: 'hour'
  333. , MINUTE: 'minute'
  334. , SECOND: 'second'
  335. , MILLISECOND: 'millisecond'
  336. , QUARTER: 'quarter'
  337. , WEEK: 'week'
  338. , WEEKDAY: 'weekday'
  339. };
  340. // Create a map for singular/plural lookup, e.g., day/days
  341. var datePartsMap = {};
  342. for (var p in dateParts) {
  343. datePartsMap[dateParts[p]] = dateParts[p];
  344. datePartsMap[dateParts[p] + 's'] = dateParts[p];
  345. }
  346. this.dateParts = dateParts;
  347. /**
  348. @name date#add
  349. @public
  350. @function
  351. @return {Date} Incremented date
  352. @description Add to a Date in intervals of different size, from
  353. milliseconds to years
  354. @param {Date} dt Date (or timestamp Number), date to increment
  355. @param {String} interv a constant representing the interval,
  356. e.g. YEAR, MONTH, DAY. See this.dateParts
  357. @param {Number} incr how much to add to the date
  358. */
  359. this.add = function (dt, interv, incr) {
  360. if (typeof dt == 'number') { dt = new Date(dt); }
  361. function fixOvershoot() {
  362. if (sum.getDate() < dt.getDate()) {
  363. sum.setDate(0);
  364. }
  365. }
  366. var key = datePartsMap[interv];
  367. var sum = new Date(dt);
  368. switch (key) {
  369. case dateParts.YEAR:
  370. sum.setFullYear(dt.getFullYear()+incr);
  371. // Keep increment/decrement from 2/29 out of March
  372. fixOvershoot();
  373. break;
  374. case dateParts.QUARTER:
  375. // Naive quarter is just three months
  376. incr*=3;
  377. // fallthrough...
  378. case dateParts.MONTH:
  379. sum.setMonth(dt.getMonth()+incr);
  380. // Reset to last day of month if you overshoot
  381. fixOvershoot();
  382. break;
  383. case dateParts.WEEK:
  384. incr*=7;
  385. // fallthrough...
  386. case dateParts.DAY:
  387. sum.setDate(dt.getDate() + incr);
  388. break;
  389. case dateParts.WEEKDAY:
  390. //FIXME: assumes Saturday/Sunday weekend, but even this is not fixed.
  391. // There are CLDR entries to localize this.
  392. var dat = dt.getDate();
  393. var weeks = 0;
  394. var days = 0;
  395. var strt = 0;
  396. var trgt = 0;
  397. var adj = 0;
  398. // Divide the increment time span into weekspans plus leftover days
  399. // e.g., 8 days is one 5-day weekspan / and two leftover days
  400. // Can't have zero leftover days, so numbers divisible by 5 get
  401. // a days value of 5, and the remaining days make up the number of weeks
  402. var mod = incr % 5;
  403. if (mod == 0) {
  404. days = (incr > 0) ? 5 : -5;
  405. weeks = (incr > 0) ? ((incr-5)/5) : ((incr+5)/5);
  406. }
  407. else {
  408. days = mod;
  409. weeks = parseInt(incr/5);
  410. }
  411. // Get weekday value for orig date param
  412. strt = dt.getDay();
  413. // Orig date is Sat / positive incrementer
  414. // Jump over Sun
  415. if (strt == 6 && incr > 0) {
  416. adj = 1;
  417. }
  418. // Orig date is Sun / negative incrementer
  419. // Jump back over Sat
  420. else if (strt == 0 && incr < 0) {
  421. adj = -1;
  422. }
  423. // Get weekday val for the new date
  424. trgt = strt + days;
  425. // New date is on Sat or Sun
  426. if (trgt == 0 || trgt == 6) {
  427. adj = (incr > 0) ? 2 : -2;
  428. }
  429. // Increment by number of weeks plus leftover days plus
  430. // weekend adjustments
  431. sum.setDate(dat + (7*weeks) + days + adj);
  432. break;
  433. case dateParts.HOUR:
  434. sum.setHours(sum.getHours()+incr);
  435. break;
  436. case dateParts.MINUTE:
  437. sum.setMinutes(sum.getMinutes()+incr);
  438. break;
  439. case dateParts.SECOND:
  440. sum.setSeconds(sum.getSeconds()+incr);
  441. break;
  442. case dateParts.MILLISECOND:
  443. sum.setMilliseconds(sum.getMilliseconds()+incr);
  444. break;
  445. default:
  446. // Do nothing
  447. break;
  448. }
  449. return sum; // Date
  450. };
  451. /**
  452. @name date#diff
  453. @public
  454. @function
  455. @return {Number} number of (interv) units apart that
  456. the two dates are
  457. @description Get the difference in a specific unit of time (e.g., number
  458. of months, weeks, days, etc.) between two dates.
  459. @param {Date} date1 First date to check
  460. @param {Date} date2 Date to compate `date1` with
  461. @param {String} interv a constant representing the interval,
  462. e.g. YEAR, MONTH, DAY. See this.dateParts
  463. */
  464. this.diff = function (date1, date2, interv) {
  465. // date1
  466. // Date object or Number equivalent
  467. //
  468. // date2
  469. // Date object or Number equivalent
  470. //
  471. // interval
  472. // A constant representing the interval, e.g. YEAR, MONTH, DAY. See this.dateParts.
  473. // Accept timestamp input
  474. if (typeof date1 == 'number') { date1 = new Date(date1); }
  475. if (typeof date2 == 'number') { date2 = new Date(date2); }
  476. var yeaDiff = date2.getFullYear() - date1.getFullYear();
  477. var monDiff = (date2.getMonth() - date1.getMonth()) + (yeaDiff * 12);
  478. var msDiff = date2.getTime() - date1.getTime(); // Millisecs
  479. var secDiff = msDiff/1000;
  480. var minDiff = secDiff/60;
  481. var houDiff = minDiff/60;
  482. var dayDiff = houDiff/24;
  483. var weeDiff = dayDiff/7;
  484. var delta = 0; // Integer return value
  485. var key = datePartsMap[interv];
  486. switch (key) {
  487. case dateParts.YEAR:
  488. delta = yeaDiff;
  489. break;
  490. case dateParts.QUARTER:
  491. var m1 = date1.getMonth();
  492. var m2 = date2.getMonth();
  493. // Figure out which quarter the months are in
  494. var q1 = Math.floor(m1/3) + 1;
  495. var q2 = Math.floor(m2/3) + 1;
  496. // Add quarters for any year difference between the dates
  497. q2 += (yeaDiff * 4);
  498. delta = q2 - q1;
  499. break;
  500. case dateParts.MONTH:
  501. delta = monDiff;
  502. break;
  503. case dateParts.WEEK:
  504. // Truncate instead of rounding
  505. // Don't use Math.floor -- value may be negative
  506. delta = parseInt(weeDiff);
  507. break;
  508. case dateParts.DAY:
  509. delta = dayDiff;
  510. break;
  511. case dateParts.WEEKDAY:
  512. var days = Math.round(dayDiff);
  513. var weeks = parseInt(days/7);
  514. var mod = days % 7;
  515. // Even number of weeks
  516. if (mod == 0) {
  517. days = weeks*5;
  518. }
  519. else {
  520. // Weeks plus spare change (< 7 days)
  521. var adj = 0;
  522. var aDay = date1.getDay();
  523. var bDay = date2.getDay();
  524. weeks = parseInt(days/7);
  525. mod = days % 7;
  526. // Mark the date advanced by the number of
  527. // round weeks (may be zero)
  528. var dtMark = new Date(date1);
  529. dtMark.setDate(dtMark.getDate()+(weeks*7));
  530. var dayMark = dtMark.getDay();
  531. // Spare change days -- 6 or less
  532. if (dayDiff > 0) {
  533. switch (true) {
  534. // Range starts on Sat
  535. case aDay == 6:
  536. adj = -1;
  537. break;
  538. // Range starts on Sun
  539. case aDay == 0:
  540. adj = 0;
  541. break;
  542. // Range ends on Sat
  543. case bDay == 6:
  544. adj = -1;
  545. break;
  546. // Range ends on Sun
  547. case bDay == 0:
  548. adj = -2;
  549. break;
  550. // Range contains weekend
  551. case (dayMark + mod) > 5:
  552. adj = -2;
  553. break;
  554. default:
  555. // Do nothing
  556. break;
  557. }
  558. }
  559. else if (dayDiff < 0) {
  560. switch (true) {
  561. // Range starts on Sat
  562. case aDay == 6:
  563. adj = 0;
  564. break;
  565. // Range starts on Sun
  566. case aDay == 0:
  567. adj = 1;
  568. break;
  569. // Range ends on Sat
  570. case bDay == 6:
  571. adj = 2;
  572. break;
  573. // Range ends on Sun
  574. case bDay == 0:
  575. adj = 1;
  576. break;
  577. // Range contains weekend
  578. case (dayMark + mod) < 0:
  579. adj = 2;
  580. break;
  581. default:
  582. // Do nothing
  583. break;
  584. }
  585. }
  586. days += adj;
  587. days -= (weeks*2);
  588. }
  589. delta = days;
  590. break;
  591. case dateParts.HOUR:
  592. delta = houDiff;
  593. break;
  594. case dateParts.MINUTE:
  595. delta = minDiff;
  596. break;
  597. case dateParts.SECOND:
  598. delta = secDiff;
  599. break;
  600. case dateParts.MILLISECOND:
  601. delta = msDiff;
  602. break;
  603. default:
  604. // Do nothing
  605. break;
  606. }
  607. // Round for fractional values and DST leaps
  608. return Math.round(delta); // Number (integer)
  609. };
  610. /**
  611. @name date#parse
  612. @public
  613. @function
  614. @return {Date} a JavaScript Date object
  615. @description Convert various sorts of strings to JavaScript
  616. Date objects
  617. @param {String} val The string to convert to a Date
  618. */
  619. this.parse = function (val, options) {
  620. var dt
  621. , opts = options || {}
  622. , matches
  623. , reordered
  624. , off
  625. , posOff
  626. , offHours
  627. , offMinutes
  628. , offSeconds
  629. , curr
  630. , stamp
  631. , utc;
  632. // Yay, we have a date, use it as-is
  633. if (val instanceof Date || typeof val.getFullYear == 'function') {
  634. dt = val;
  635. }
  636. // Timestamp?
  637. else if (typeof val == 'number') {
  638. dt = new Date(val);
  639. }
  640. // String or Array
  641. else {
  642. // Value preparsed, looks like [yyyy, mo, dd, hh, mi, ss, ms, (offset?)]
  643. if (_isArray(val)) {
  644. matches = val;
  645. matches.unshift(null);
  646. matches[8] = null;
  647. }
  648. // Oh, crap, it's a string -- parse this bitch
  649. else if (typeof val == 'string') {
  650. matches = val.match(_DATETIME_PAT);
  651. // Stupid US-only format?
  652. if (!matches) {
  653. matches = val.match(_US_DATE_PAT);
  654. if (matches) {
  655. reordered = [matches[0], matches[3], matches[1], matches[2]];
  656. // Pad the results to the same length as ISO8601
  657. reordered[8] = null;
  658. matches = reordered;
  659. }
  660. }
  661. // Time-stored-in-Date hack?
  662. if (!matches) {
  663. matches = val.match(_TIME_PAT);
  664. if (matches) {
  665. reordered = [matches[0], 0, 1, 0, matches[1],
  666. matches[2], matches[3], matches[4], null];
  667. matches = reordered;
  668. }
  669. }
  670. }
  671. // Sweet, the regex actually parsed it into something useful
  672. if (matches) {
  673. matches.shift(); // First match is entire match, DO NOT WANT
  674. off = matches.pop();
  675. // If there's an offset (or the 'Z' non-offset offset), use UTC
  676. // methods to set everything
  677. if (off) {
  678. if (off == 'Z') {
  679. utc = true;
  680. offSeconds = 0;
  681. }
  682. else {
  683. utc = false;
  684. // Convert from extended to basic if necessary
  685. off = off.replace(/:/g, '');
  686. // '+0000' will still be zero
  687. if (parseInt(off, 10) === 0) {
  688. utc = true;
  689. }
  690. else {
  691. posOff = off.indexOf('+') === 0;
  692. // Strip plus or minus
  693. off = off.substr(1);
  694. offHours = parseInt(off.substr(0, 2), 10);
  695. offMinutes = off.substr(2, 2);
  696. if (offMinutes) {
  697. offMinutes = parseInt(offMinutes, 10);
  698. }
  699. else {
  700. offMinutes = 0;
  701. }
  702. offSeconds = off.substr(4, 2);
  703. if (offSeconds) {
  704. offSeconds = parseInt(offSeconds, 10);
  705. }
  706. else {
  707. offSeconds = 0;
  708. }
  709. offSeconds += (offMinutes * 60)
  710. offSeconds += (offHours * 60 * 60);
  711. if (!posOff) {
  712. offSeconds = 0 - offSeconds;
  713. }
  714. }
  715. }
  716. }
  717. dt = new Date(0);
  718. // Stupid zero-based months
  719. matches[1] = parseInt(matches[1], 10) - 1;
  720. // Specific offset, iterate the array and set each date property
  721. // using UTC setters, then adjust time using offset
  722. if (off) {
  723. for (var i = matches.length - 1; i > -1; i--) {
  724. curr = parseInt(matches[i], 10) || 0;
  725. dt['setUTC' + _dateMethods[i]](curr);
  726. }
  727. // Add any offset
  728. dt.setSeconds(dt.getSeconds() - offSeconds);
  729. }
  730. // Otherwise we know nothing about the offset, just iterate the
  731. // array and set each date property using regular setters
  732. else {
  733. var lastValIndex;
  734. for (var i = matches.length - 1; i > -1; i--) {
  735. if (matches[i]) {
  736. curr = parseInt(matches[i], 10);
  737. if (typeof lastValIndex == 'undefined') {
  738. lastValIndex = i;
  739. }
  740. }
  741. else {
  742. curr = 0;
  743. }
  744. dt['set' + _dateMethods[i]](curr);
  745. }
  746. if (opts.setMax) {
  747. for (var i = lastValIndex + 1, ii = matches.length; i < ii; i++) {
  748. switch (i) {
  749. case 3:
  750. dt['set' + _dateMethods[i]](23);
  751. break;
  752. case 4:
  753. case 5:
  754. dt['set' + _dateMethods[i]](59);
  755. break;
  756. case 6:
  757. dt.setMilliseconds(999);
  758. break;
  759. }
  760. }
  761. }
  762. }
  763. }
  764. // Shit, last-ditch effort using Date.parse
  765. else {
  766. stamp = Date.parse(val);
  767. // Failures to parse yield NaN
  768. if (!isNaN(stamp)) {
  769. dt = new Date(stamp);
  770. }
  771. }
  772. }
  773. return dt || null;
  774. };
  775. /**
  776. @name date#relativeTime
  777. @public
  778. @function
  779. @return {String} A string describing the amount of time ago
  780. the passed-in Date is
  781. @description Convert a Date to an English sentence representing
  782. how long ago the Date was
  783. @param {Date} dt The Date to to convert to a relative time string
  784. @param {Object} [opts]
  785. @param {Boolean} [opts.abbreviated=false] Use short strings
  786. (e.g., '<1m') for the relative-time string
  787. */
  788. this.relativeTime = function (dt, options) {
  789. var opts = options || {}
  790. , now = opts.now || new Date()
  791. , abbr = opts.abbreviated || false
  792. , format = opts.format || '%F %T'
  793. // Diff in seconds
  794. , diff = (now.getTime() - dt.getTime()) / 1000
  795. , ret
  796. , num
  797. , hour = 60*60
  798. , day = 24*hour
  799. , week = 7*day
  800. , month = 30*day;
  801. switch (true) {
  802. case diff < 60:
  803. ret = abbr ? '<1m' : 'less than a minute ago';
  804. break;
  805. case diff < 120:
  806. ret = abbr ? '1m' : 'about a minute ago';
  807. break;
  808. case diff < (45*60):
  809. num = parseInt((diff / 60), 10);
  810. ret = abbr ? num + 'm' : num + ' minutes ago';
  811. break;
  812. case diff < (2*hour):
  813. ret = abbr ? '1h' : 'about an hour ago';
  814. break;
  815. case diff < (1*day):
  816. num = parseInt((diff / hour), 10);
  817. ret = abbr ? num + 'h' : 'about ' + num + ' hours ago';
  818. break;
  819. case diff < (2*day):
  820. ret = abbr ? '1d' : 'one day ago';
  821. break;
  822. case diff < (7*day):
  823. num = parseInt((diff / day), 10);
  824. ret = abbr ? num + 'd' : 'about ' + num + ' days ago';
  825. break;
  826. case diff < (11*day):
  827. ret = abbr ? '1w': 'one week ago';
  828. break;
  829. case diff < (1*month):
  830. num = Math.round(diff / week);
  831. ret = abbr ? num + 'w' : 'about ' + num + ' weeks ago';
  832. break;
  833. default:
  834. ret = date.strftime(dt, format);
  835. break;
  836. }
  837. return ret;
  838. };
  839. /**
  840. @name date#toISO8601
  841. @public
  842. @function
  843. @return {String} A string describing the amount of time ago
  844. @description Convert a Date to an ISO8601-formatted string
  845. @param {Date} dt The Date to to convert to an ISO8601 string
  846. */
  847. var _pad = function (n) {
  848. return n < 10 ? '0' + n : n;
  849. };
  850. this.toISO8601 = function (dt, options) {
  851. var opts = options || {}
  852. , off = dt.getTimezoneOffset()
  853. , offHours
  854. , offMinutes
  855. , str = this.strftime(dt, '%F') + 'T'
  856. + this.strftime(dt, '%T') + '.'
  857. + string.lpad(dt.getMilliseconds(), '0', 3);
  858. if (opts.tz) {
  859. // Pos and neg numbers are both truthy; only
  860. // zero is falsy
  861. if (off && !opts.utc) {
  862. str += off > 0 ? '-' : '+';
  863. offHours = parseInt(off / 60, 10);
  864. str += string.lpad(offHours, '0', 2);
  865. offMinutes = off % 60;
  866. if (offMinutes) {
  867. str += string.lpad(offMinutes, '0', 2);
  868. }
  869. }
  870. else {
  871. str += 'Z';
  872. }
  873. }
  874. return str;
  875. };
  876. // Alias
  877. this.toIso8601 = this.toISO8601;
  878. this.toUTC = function (dt) {
  879. return new Date(
  880. dt.getUTCFullYear()
  881. , dt.getUTCMonth()
  882. , dt.getUTCDate()
  883. , dt.getUTCHours()
  884. , dt.getUTCMinutes()
  885. , dt.getUTCSeconds()
  886. , dt.getUTCMilliseconds());
  887. };
  888. })();
  889. module.exports = date;