index.js 6.3 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. 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 matchers = [
  66. {
  67. pattern: /y{1,4}/,
  68. regexp: "\\d{1,4}",
  69. fn: function(date, value) {
  70. setDatePart(date, 'FullYear', value, local);
  71. }
  72. },
  73. {
  74. pattern: /MM/,
  75. regexp: "\\d{1,2}",
  76. fn: function(date, value) {
  77. setDatePart(date, 'Month', (value - 1), local);
  78. }
  79. },
  80. {
  81. pattern: /dd/,
  82. regexp: "\\d{1,2}",
  83. fn: function(date, value) {
  84. setDatePart(date, 'Date', value, local);
  85. }
  86. },
  87. {
  88. pattern: /hh/,
  89. regexp: "\\d{1,2}",
  90. fn: function(date, value) {
  91. setDatePart(date, 'Hours', value, local);
  92. }
  93. },
  94. {
  95. pattern: /mm/,
  96. regexp: "\\d\\d",
  97. fn: function(date, value) {
  98. setDatePart(date, 'Minutes', value, local);
  99. }
  100. },
  101. {
  102. pattern: /ss/,
  103. regexp: "\\d\\d",
  104. fn: function(date, value) {
  105. setDatePart(date, 'Seconds', value, local);
  106. }
  107. },
  108. {
  109. pattern: /SSS/,
  110. regexp: "\\d\\d\\d",
  111. fn: function(date, value) {
  112. setDatePart(date, 'Milliseconds', value, local);
  113. }
  114. },
  115. {
  116. pattern: /O/,
  117. regexp: "[+-]\\d{1,2}:?\\d{2}?|Z",
  118. fn: function(date, value) {
  119. if (value === "Z") {
  120. value = 0;
  121. }
  122. else {
  123. value = value.replace(":", "");
  124. }
  125. var offset = Math.abs(value);
  126. var timezoneOffset = (value > 0 ? -1 : 1 ) * ((offset % 100) + Math.floor(offset / 100) * 60);
  127. // Per ISO8601 standard: UTC = local time - offset
  128. //
  129. // For example, 2000-01-01T01:00:00-0700
  130. // local time: 2000-01-01T01:00:00
  131. // ==> UTC : 2000-01-01T08:00:00 ( 01 - (-7) = 8 )
  132. //
  133. // To make it even more confusing, the date.getTimezoneOffset() is
  134. // opposite sign of offset string in the ISO8601 standard. So if offset
  135. // is '-0700' the getTimezoneOffset() would be (+)420. The line above
  136. // calculates timezoneOffset to matche Javascript's behavior.
  137. //
  138. // The date/time of the input is actually the local time, so the date
  139. // object that was constructed is actually local time even thought the
  140. // UTC setters are used. This means the date object's internal UTC
  141. // representation was wrong. It needs to be fixed by substracting the
  142. // offset (or adding the offset minutes as they are opposite sign).
  143. //
  144. // Note: the time zone has to be processed after all other fileds are
  145. // set. The result would be incorrect if the offset was calculated
  146. // first then overriden by the other filed setters.
  147. date.setUTCMinutes(date.getUTCMinutes() + timezoneOffset);
  148. }
  149. }
  150. ];
  151. var parsedPattern = matchers.reduce(
  152. function(p, m) {
  153. if (m.pattern.test(p.regexp)) {
  154. m.index = p.regexp.match(m.pattern).index;
  155. p.regexp = p.regexp.replace(m.pattern, "(" + m.regexp + ")");
  156. } else {
  157. m.index = -1;
  158. }
  159. return p;
  160. },
  161. { regexp: pattern, index: [] }
  162. );
  163. var dateFns = matchers.filter(function(m) {
  164. return m.index > -1;
  165. });
  166. dateFns.sort(function(a, b) {
  167. return a.index - b.index;
  168. });
  169. var matcher = new RegExp(parsedPattern.regexp);
  170. var matches = matcher.exec(str);
  171. if (matches) {
  172. var date = missingValuesDate || module.exports.now();
  173. dateFns.forEach(function(f, i) {
  174. f.fn(date, matches[i + 1]);
  175. });
  176. return date;
  177. }
  178. throw new Error(
  179. "String '" + str + "' could not be parsed as '" + pattern + "'"
  180. );
  181. }
  182. function parse(pattern, str, missingValuesDate) {
  183. if (!pattern) {
  184. throw new Error("pattern must be supplied");
  185. }
  186. return extractDateParts(pattern, str, missingValuesDate);
  187. }
  188. /**
  189. * Used for testing - replace this function with a fixed date.
  190. */
  191. // istanbul ignore next
  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";