index.js 6.2 KB

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