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