index.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. "use strict";
  2. function padWithZeros(vNumber, width) {
  3. var numAsString = vNumber.toString();
  4. while (numAsString.length < width) {
  5. numAsString = "0" + numAsString;
  6. }
  7. return numAsString;
  8. }
  9. function addZero(vNumber) {
  10. return padWithZeros(vNumber, 2);
  11. }
  12. /**
  13. * Formats the TimeOffset
  14. * Thanks to http://www.svendtofte.com/code/date_format/
  15. * @private
  16. */
  17. function offset(timezoneOffset) {
  18. var os = Math.abs(timezoneOffset);
  19. var h = String(Math.floor(os / 60));
  20. var m = String(os % 60);
  21. h = ("0" + h).slice(-2);
  22. m = ("0" + m).slice(-2);
  23. return timezoneOffset === 0 ? "Z" : (timezoneOffset < 0 ? "+" : "-") + h + ":" + m;
  24. }
  25. function asString(format, date) {
  26. if (typeof format !== "string") {
  27. date = format;
  28. format = module.exports.ISO8601_FORMAT;
  29. }
  30. if (!date) {
  31. date = module.exports.now();
  32. }
  33. // Issue # 14 - Per ISO8601 standard, the time string should be local time
  34. // with timezone info.
  35. // See https://en.wikipedia.org/wiki/ISO_8601 section "Time offsets from UTC"
  36. var vDay = addZero(date.getDate());
  37. var vMonth = addZero(date.getMonth() + 1);
  38. var vYearLong = addZero(date.getFullYear());
  39. var vYearShort = addZero(vYearLong.substring(2, 4));
  40. var vYear = format.indexOf("yyyy") > -1 ? vYearLong : vYearShort;
  41. var vHour = addZero(date.getHours());
  42. var vMinute = addZero(date.getMinutes());
  43. var vSecond = addZero(date.getSeconds());
  44. var vMillisecond = padWithZeros(date.getMilliseconds(), 3);
  45. var vTimeZone = offset(date.getTimezoneOffset());
  46. var formatted = format
  47. .replace(/dd/g, vDay)
  48. .replace(/MM/g, vMonth)
  49. .replace(/y{1,4}/g, vYear)
  50. .replace(/hh/g, vHour)
  51. .replace(/mm/g, vMinute)
  52. .replace(/ss/g, vSecond)
  53. .replace(/SSS/g, vMillisecond)
  54. .replace(/O/g, vTimeZone);
  55. return formatted;
  56. }
  57. function setDatePart(date, part, value, local) {
  58. date['set' + (local ? '' : 'UTC') + part](value);
  59. }
  60. function extractDateParts(pattern, str, missingValuesDate) {
  61. // Javascript Date object doesn't support custom timezone. Sets all felds as
  62. // GMT based to begin with. If the timezone offset is provided, then adjust
  63. // it using provided timezone, otherwise, adjust it with the system timezone.
  64. var local = pattern.indexOf('O') < 0;
  65. var monthOverflow = false;
  66. var matchers = [
  67. {
  68. pattern: /y{1,4}/,
  69. regexp: "\\d{1,4}",
  70. fn: function(date, value) {
  71. setDatePart(date, 'FullYear', value, local);
  72. }
  73. },
  74. {
  75. pattern: /MM/,
  76. regexp: "\\d{1,2}",
  77. fn: function(date, value) {
  78. setDatePart(date, 'Month', (value - 1), local);
  79. if (date.getMonth() !== (value - 1)) {
  80. // in the event of 31 May --> 31 Feb --> 3 Mar
  81. // this is correct behavior if no Date is involved
  82. monthOverflow = true;
  83. }
  84. }
  85. },
  86. {
  87. pattern: /dd/,
  88. regexp: "\\d{1,2}",
  89. fn: function(date, value) {
  90. // in the event of 31 May --> 31 Feb --> 3 Mar
  91. // reset Mar back to Feb, before setting the Date
  92. if (monthOverflow) {
  93. setDatePart(date, 'Month', (date.getMonth() - 1), local);
  94. }
  95. setDatePart(date, 'Date', value, local);
  96. }
  97. },
  98. {
  99. pattern: /hh/,
  100. regexp: "\\d{1,2}",
  101. fn: function(date, value) {
  102. setDatePart(date, 'Hours', value, local);
  103. }
  104. },
  105. {
  106. pattern: /mm/,
  107. regexp: "\\d\\d",
  108. fn: function(date, value) {
  109. setDatePart(date, 'Minutes', value, local);
  110. }
  111. },
  112. {
  113. pattern: /ss/,
  114. regexp: "\\d\\d",
  115. fn: function(date, value) {
  116. setDatePart(date, 'Seconds', value, local);
  117. }
  118. },
  119. {
  120. pattern: /SSS/,
  121. regexp: "\\d\\d\\d",
  122. fn: function(date, value) {
  123. setDatePart(date, 'Milliseconds', value, local);
  124. }
  125. },
  126. {
  127. pattern: /O/,
  128. regexp: "[+-]\\d{1,2}:?\\d{2}?|Z",
  129. fn: function(date, value) {
  130. if (value === "Z") {
  131. value = 0;
  132. }
  133. else {
  134. value = value.replace(":", "");
  135. }
  136. var offset = Math.abs(value);
  137. var timezoneOffset = (value > 0 ? -1 : 1 ) * ((offset % 100) + Math.floor(offset / 100) * 60);
  138. // Per ISO8601 standard: UTC = local time - offset
  139. //
  140. // For example, 2000-01-01T01:00:00-0700
  141. // local time: 2000-01-01T01:00:00
  142. // ==> UTC : 2000-01-01T08:00:00 ( 01 - (-7) = 8 )
  143. //
  144. // To make it even more confusing, the date.getTimezoneOffset() is
  145. // opposite sign of offset string in the ISO8601 standard. So if offset
  146. // is '-0700' the getTimezoneOffset() would be (+)420. The line above
  147. // calculates timezoneOffset to matche Javascript's behavior.
  148. //
  149. // The date/time of the input is actually the local time, so the date
  150. // object that was constructed is actually local time even thought the
  151. // UTC setters are used. This means the date object's internal UTC
  152. // representation was wrong. It needs to be fixed by substracting the
  153. // offset (or adding the offset minutes as they are opposite sign).
  154. //
  155. // Note: the time zone has to be processed after all other fields are
  156. // set. The result would be incorrect if the offset was calculated
  157. // first then overriden by the other filed setters.
  158. date.setUTCMinutes(date.getUTCMinutes() + timezoneOffset);
  159. }
  160. }
  161. ];
  162. var parsedPattern = matchers.reduce(
  163. function(p, m) {
  164. if (m.pattern.test(p.regexp)) {
  165. m.index = p.regexp.match(m.pattern).index;
  166. p.regexp = p.regexp.replace(m.pattern, "(" + m.regexp + ")");
  167. } else {
  168. m.index = -1;
  169. }
  170. return p;
  171. },
  172. { regexp: pattern, index: [] }
  173. );
  174. var dateFns = matchers.filter(function(m) {
  175. return m.index > -1;
  176. });
  177. dateFns.sort(function(a, b) {
  178. return a.index - b.index;
  179. });
  180. var matcher = new RegExp(parsedPattern.regexp);
  181. var matches = matcher.exec(str);
  182. if (matches) {
  183. var date = missingValuesDate || module.exports.now();
  184. dateFns.forEach(function(f, i) {
  185. f.fn(date, matches[i + 1]);
  186. });
  187. return date;
  188. }
  189. throw new Error(
  190. "String '" + str + "' could not be parsed as '" + pattern + "'"
  191. );
  192. }
  193. function parse(pattern, str, missingValuesDate) {
  194. if (!pattern) {
  195. throw new Error("pattern must be supplied");
  196. }
  197. return extractDateParts(pattern, str, missingValuesDate);
  198. }
  199. /**
  200. * Used for testing - replace this function with a fixed date.
  201. */
  202. function now() {
  203. return new Date();
  204. }
  205. module.exports = asString;
  206. module.exports.asString = asString;
  207. module.exports.parse = parse;
  208. module.exports.now = now;
  209. module.exports.ISO8601_FORMAT = "yyyy-MM-ddThh:mm:ss.SSS";
  210. module.exports.ISO8601_WITH_TZ_OFFSET_FORMAT = "yyyy-MM-ddThh:mm:ss.SSSO";
  211. module.exports.DATETIME_FORMAT = "dd MM yyyy hh:mm:ss.SSS";
  212. module.exports.ABSOLUTETIME_FORMAT = "hh:mm:ss.SSS";