commit
message
Refactoring date string parsing to be a utility because it's required by other functions.
author
Ben Vogt <[email protected]>
date
2017-04-02 18:41:37
stats
3 file(s) changed,
835 insertions(+),
712 deletions(-)
files
src/RawFormulas/Date.ts
src/RawFormulas/Utils.ts
tests/DateFormulasTest.ts
1diff --git a/src/RawFormulas/Date.ts b/src/RawFormulas/Date.ts
2index e01db5b..137b65d 100644
3--- a/src/RawFormulas/Date.ts
4+++ b/src/RawFormulas/Date.ts
5@@ -43,54 +43,6 @@ var DATE = function (...values) : ExcelDate {
6 return excelDate;
7 };
8
9-
10-const YEAR_MONTHDIG_DAY = DateRegExBuilder.DateRegExBuilder()
11- .start()
12- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY().FLEX_DELIMITER_LOOSEDOT().MM().FLEX_DELIMITER_LOOSEDOT().DD_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
13- .end()
14- .build();
15-const MONTHDIG_DAY_YEAR = DateRegExBuilder.DateRegExBuilder()
16- .start()
17- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MM().FLEX_DELIMITER_LOOSEDOT().DD().FLEX_DELIMITER_LOOSEDOT().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
18- .end()
19- .build();
20-const DAY_MONTHNAME_YEAR = DateRegExBuilder.DateRegExBuilder()
21- .start()
22- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().DD().FLEX_DELIMITER_LOOSEDOT().MONTHNAME().FLEX_DELIMITER_LOOSEDOT().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
23- .end()
24- .build();
25-const MONTHNAME_DAY_YEAR = DateRegExBuilder.DateRegExBuilder()
26- .start()
27- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MONTHNAME().FLEX_DELIMITER_LOOSEDOT().DD().FLEX_DELIMITER_LOOSEDOT().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
28- .end()
29- .build();
30-const YEAR_MONTHDIG = DateRegExBuilder.DateRegExBuilder()
31- .start()
32- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY14().FLEX_DELIMITER().MM_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
33- .end()
34- .build();
35-const MONTHDIG_YEAR = DateRegExBuilder.DateRegExBuilder()
36- .start()
37- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MM().FLEX_DELIMITER().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
38- .end()
39- .build();
40-const YEAR_MONTHNAME = DateRegExBuilder.DateRegExBuilder()
41- .start()
42- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY14().FLEX_DELIMITER().MONTHNAME_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
43- .end()
44- .build();
45-const MONTHNAME_YEAR = DateRegExBuilder.DateRegExBuilder()
46- .start()
47- .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MONTHNAME().FLEX_DELIMITER().YYYY2_OR_4_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
48- .end()
49- .build();
50-// For reference: https://regex101.com/r/47GARA/1/
51-const TIMESTAMP = DateRegExBuilder.DateRegExBuilder()
52- .start()
53- .TIMESTAMP_UNITS_CAPTURE_GROUP()
54- .end()
55- .build();
56-
57 /**
58 * Converts a provided date string in a known format to a date value.
59 * @param values[0] date_string - The string representing the date. Understood formats include any date format which is
60@@ -100,243 +52,17 @@ const TIMESTAMP = DateRegExBuilder.DateRegExBuilder()
61 * @constructor
62 */
63 var DATEVALUE = function (...values) : number {
64- const FIRST_YEAR = 1900;
65- const Y2K_YEAR = 2000;
66 ArgsChecker.checkLength(values, 1);
67 var dateString = TypeCaster.firstValueAsString(values[0]);
68- var m;
69-
70- /**
71- * Creates moment object from years, months and days.
72- * @param years of moment
73- * @param months of moment in number or string format (eg: January)
74- * @param days of moment
75- * @returns {Moment} created moment
76- */
77- function createMoment(years, months, days) : moment.Moment {
78- var actualYear = years;
79- if (years >= 0 && years < 30) {
80- actualYear = Y2K_YEAR + years;
81- } else if (years >= 30 && years < 100) {
82- actualYear = FIRST_YEAR + years;
83- }
84- var tmpMoment = moment.utc([actualYear]).startOf("year");
85- if (typeof months === "string") {
86- tmpMoment.month(months);
87- } else {
88- tmpMoment.set("months", months);
89- }
90- // If we're specifying more days than there are in this month
91- if (days > tmpMoment.daysInMonth() - 1) {
92- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
93- }
94- return tmpMoment.add(days, 'days');
95- }
96-
97- /**
98- * Matches a timestamp string, adding the units to the moment passed in.
99- * @param timestampString to parse. ok formats: "10am", "10:10", "10:10am", "10:10:10", "10:10:10am", etc.
100- * @param momentToMutate to mutate
101- * @returns {Moment} mutated and altered.
102- */
103- function matchTimestampAndMutateMoment(timestampString : string, momentToMutate: moment.Moment) : moment.Moment {
104- var matches = timestampString.match(TIMESTAMP);
105- if (matches && matches[1] !== undefined) { // 10am
106- var hours = parseInt(matches[2]);
107- if (hours > 12) {
108- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
109- }
110- // No op on momentToMutate because you can't overload hours with am/pm.
111- } else if (matches && matches[6] !== undefined) { // 10:10
112- var hours = parseInt(matches[7]);
113- var minutes = parseInt(matches[8]);
114- momentToMutate.add(hours, 'hours').add(minutes, 'minutes');
115- } else if (matches && matches[11] !== undefined) { // 10:10am
116- var hours = parseInt(matches[13]);
117- var minutes = parseInt(matches[14]);
118- var pmTrue = (matches[16].toLowerCase() === "pm");
119- if (hours > 12) {
120- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
121- }
122- if (pmTrue) {
123- // 12pm is just 0am, 4pm is 16, etc.
124- momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
125- } else {
126- if (hours !== 12) {
127- momentToMutate.set('hours', hours);
128- }
129- }
130- momentToMutate.add(minutes, 'minutes');
131- } else if (matches && matches[17] !== undefined) { // 10:10:10
132- var hours = parseInt(matches[19]);
133- var minutes = parseInt(matches[20]);
134- var seconds = parseInt(matches[21]);
135- momentToMutate.add(hours, 'hours').add(minutes, 'minutes').add(seconds, 'seconds');
136- } else if (matches && matches[23] !== undefined) { // // 10:10:10am
137- var hours = parseInt(matches[25]);
138- var minutes = parseInt(matches[26]);
139- var seconds = parseInt(matches[27]);
140- var pmTrue = (matches[28].toLowerCase() === "pm");
141- if (hours > 12) {
142- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
143- }
144- if (pmTrue) {
145- // 12pm is just 0am, 4pm is 16, etc.
146- momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
147- } else {
148- if (hours !== 12) {
149- momentToMutate.set('hours', hours);
150- }
151- }
152- momentToMutate.add(minutes, 'minutes').add(seconds, 'seconds');
153- } else {
154- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
155- }
156- return momentToMutate.set('hours', 0).set('minutes', 0).set('seconds', 0);
157- }
158-
159- // Check YEAR_MONTHDIG, YYYY(fd)MM, '1992/06'
160- // NOTE: Must come before YEAR_MONTHDIG_DAY matching.
161- if (m === undefined) {
162- var matches = dateString.match(YEAR_MONTHDIG);
163- if (matches && matches.length >= 6) {
164- var years = parseInt(matches[3]);
165- var months = parseInt(matches[5]) - 1; // Months are zero indexed.
166- var tmpMoment = createMoment(years, months, 0);
167- if (matches[6] !== undefined) {
168- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
169- }
170- m = tmpMoment;
171- }
172- }
173-
174- // Check YEAR_MONTHDIG_DAY, YYYY(fd)MM(fd)DD, "1992/06/24"
175- if (m === undefined) {
176- var matches = dateString.match(YEAR_MONTHDIG_DAY);
177- if (matches && matches.length >= 8) {
178- // Check delimiters. If they're not the same, throw error.
179- if (matches[4].replace(/\s*/g, '') !== matches[6].replace(/\s*/g, '')) {
180- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
181- }
182- var years = parseInt(matches[3]);
183- var months = parseInt(matches[5]) - 1; // Months are zero indexed.
184- var days = parseInt(matches[7]) - 1; // Days are zero indexed.
185- var tmpMoment = createMoment(years, months, days);
186- if (matches.length >= 9 && matches[8] !== undefined) {
187- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
188- }
189- m = tmpMoment;
190- }
191- }
192-
193- // Check MONTHDIG_YEAR, MM(fd)YYYY, '06/1992'
194- // NOTE: Must come before MONTHDIG_DAY_YEAR matching.
195- if (m === undefined) {
196- var matches = dateString.match(MONTHDIG_YEAR);
197- if (matches && matches.length >= 6) {
198- var years = parseInt(matches[5]);
199- var months = parseInt(matches[3]) - 1; // Months are zero indexed.
200- var tmpMoment = createMoment(years, months, 0);
201- if (matches[6] !== undefined) {
202- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
203- }
204- m = tmpMoment;
205- }
206- }
207-
208- // Check MONTHDIG_DAY_YEAR, MM(fd)DD(fd)YYYY, "06/24/1992"
209- if (m === undefined) {
210- var matches = dateString.match(MONTHDIG_DAY_YEAR);
211- if (matches && matches.length >= 8) {
212- // Check delimiters. If they're not the same, throw error.
213- if (matches[4].replace(/\s*/g, '') !== matches[6].replace(/\s*/g, '')) {
214- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
215- }
216- var years = parseInt(matches[7]);
217- var months = parseInt(matches[3]) - 1; // Months are zero indexed.
218- var days = parseInt(matches[5]) - 1; // Days are zero indexed.
219- var tmpMoment = createMoment(years, months, days);
220- if (matches.length >= 9 && matches[8] !== undefined) {
221- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
222- }
223- m = tmpMoment;
224- }
225- }
226-
227- // Check MONTHNAME_YEAR, Month(fd)YYYY, 'Aug 1992'
228- // NOTE: Needs to come before DAY_MONTHNAME_YEAR matching.
229- if (m === undefined) {
230- var matches = dateString.match(MONTHNAME_YEAR);
231- if (matches && matches.length >= 6) {
232- var years = parseInt(matches[5]);
233- var monthName = matches[3];
234- var tmpMoment = createMoment(years, monthName, 0);
235- if (matches[6] !== undefined) {
236- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
237- }
238- m = tmpMoment;
239- }
240- }
241-
242- // Check MONTHNAME_DAY_YEAR, Month(fd)DD(fd)YYYY, 'Aug 19 2020'
243- if (m === undefined) {
244- var matches = dateString.match(MONTHNAME_DAY_YEAR);
245- if (matches && matches.length >= 8) {
246- // Check delimiters. If they're not the same, throw error.
247- if (matches[4].replace(/\s*/g, '') !== matches[6].replace(/\s*/g, '')) {
248- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
249- }
250- var years = parseInt(matches[7]);
251- var monthName = matches[3];
252- var days = parseInt(matches[5]) - 1; // Days are zero indexed.
253- var tmpMoment = createMoment(years, monthName, days);
254- if (matches.length >= 9 && matches[8] !== undefined) {
255- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
256- }
257- m = tmpMoment;
258- }
259- }
260-
261- // Check DAY_MONTHNAME_YEAR, DD(fd)Month(fd)YYYY, '24/July/1992'
262- if (m === undefined) {
263- var matches = dateString.match(DAY_MONTHNAME_YEAR);
264- if (matches && matches.length >= 8) {
265- var years = parseInt(matches[7]);
266- var monthName = matches[5];
267- var days = parseInt(matches[3]) - 1; // Days are zero indexed.
268- var firstDelimiter = matches[4].replace(/\s*/g, '');
269- var secondDelimiter = matches[6].replace(/\s*/g, '');
270- // Check delimiters. If they're not the same, and the first one isn't a space, throw error.
271- if (firstDelimiter !== secondDelimiter && firstDelimiter !== "") {
272- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
273- }
274- var tmpMoment = createMoment(years, monthName, days);
275- if (matches.length >= 9 && matches[8] !== undefined) {
276- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
277- }
278- m = tmpMoment;
279- }
280- }
281-
282- // Check YEAR_MONTHNAME, YYYY(fd)Month, '1992/Aug'
283- if (m === undefined) {
284- var matches = dateString.match(YEAR_MONTHNAME);
285- if (matches && matches.length >= 6) {
286- var years = parseInt(matches[3]);
287- var monthName = matches[5];
288- var tmpMoment = createMoment(years, monthName, 0);
289- if (matches[6] !== undefined) {
290- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
291- }
292- m = tmpMoment;
293- }
294+ var date;
295+ try {
296+ date = TypeCaster.stringToExcelDate(dateString);
297+ } catch (e) {
298+ throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
299 }
300
301 // If we've not been able to parse the date by now, then we cannot parse it at all.
302- if (m === undefined || !m.isValid()) {
303- throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
304- }
305- return new ExcelDate(m).toNumber();
306+ return date.toNumber();
307 };
308
309
310diff --git a/src/RawFormulas/Utils.ts b/src/RawFormulas/Utils.ts
311index 1618a7e..4e4dd5a 100644
312--- a/src/RawFormulas/Utils.ts
313+++ b/src/RawFormulas/Utils.ts
314@@ -1,4 +1,6 @@
315-import { CellError } from "../Errors"
316+/// <reference path="../../node_modules/moment/moment.d.ts"/>
317+import * as moment from "moment";
318+import {CellError, VALUE_ERROR} from "../Errors"
319 import * as ERRORS from "../Errors"
320 import {ExcelDate} from "../ExcelDate";
321
322@@ -21,6 +23,252 @@ function wildCardRegex(c: string) {
323 return new RegExp("^"+d.join(".*")+"$", "g");
324 }
325
326+/**
327+ * Build a regular expression step by step, to make it easier to build and read the resulting regular expressions.
328+ */
329+class DateRegExBuilder {
330+ private regexString = "";
331+ private static ZERO_OR_MORE_SPACES = "\\s*";
332+
333+ static DateRegExBuilder() : DateRegExBuilder {
334+ return new DateRegExBuilder();
335+ }
336+
337+ /**
338+ * Start the regular expression builder by matching the start of a line and zero or more spaces.
339+ * @returns {DateRegExBuilder} builder
340+ */
341+ start() : DateRegExBuilder {
342+ this.regexString += "^" + DateRegExBuilder.ZERO_OR_MORE_SPACES;
343+ return this;
344+ }
345+
346+ /**
347+ * End the regular expression builder by matching the end of the line.
348+ * @returns {DateRegExBuilder} builder
349+ */
350+ end(): DateRegExBuilder {
351+ this.regexString += "$";
352+ return this;
353+ }
354+
355+ /**
356+ * Capture all month full name and short names to the regular expression.
357+ * @returns {DateRegExBuilder} builder
358+ */
359+ MONTHNAME() : DateRegExBuilder {
360+ this.regexString += "(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)";
361+ return this;
362+ }
363+
364+ /**
365+ * Capture all month full name and short names to the regular expression, in addition to any followed by one or more
366+ * spaces.
367+ * @returns {DateRegExBuilder} builder
368+ * @constructor
369+ */
370+ MONTHNAME_W_SPACE() : DateRegExBuilder {
371+ this.regexString += "(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec|january\\s+|february\\s+|march\\s+|april\\s+|may\\s+|june\\s+|july\\s+|august\\s+|september\\s+|october\\s+|november\\s+|december\\s+|jan\\s+|feb\\s+|mar\\s+|apr\\s+|jun\\s+|jul\\s+|aug\\s+|sep\\s+|oct\\s+|nov\\s+|dec\\s+)";
372+ return this;
373+ }
374+
375+ /**
376+ * Add capture group for optionally capturing day names.
377+ * @returns {DateRegExBuilder} builder
378+ * @constructor
379+ */
380+ OPTIONAL_DAYNAME() : DateRegExBuilder {
381+ this.regexString += "(sunday|monday|tuesday|wednesday|thursday|friday|saturday|sun|mon|tue|wed|thu|fri|sat)?";
382+ return this;
383+ }
384+
385+ /**
386+ * Add capture group for optionally capturing a comma followed by one or more spaces.
387+ * @returns {DateRegExBuilder} builder
388+ * @constructor
389+ */
390+ OPTIONAL_COMMA() : DateRegExBuilder {
391+ this.regexString += "(,?\\s+)?";
392+ return this;
393+ }
394+
395+ /**
396+ * Add capture group for capturing month digits between 01 and 12, inclusively.
397+ * @returns {DateRegExBuilder} builder
398+ * @constructor
399+ */
400+ MM() : DateRegExBuilder {
401+ this.regexString += "([1-9]|0[1-9]|1[0-2])";
402+ return this;
403+ }
404+
405+ /**
406+ * Add capture group for capturing month digits between 01 and 12, inclusively, in addition to any followed by one or
407+ * more spaces.
408+ * @returns {DateRegExBuilder} builder
409+ * @constructor
410+ */
411+ MM_W_SPACE() : DateRegExBuilder {
412+ this.regexString += "([1-9]|0[1-9]|1[0-2]|[1-9]\\s+|0[1-9]\\s+|1[0-2]\\s+)";
413+ return this;
414+ }
415+
416+ /**
417+ * Add capture group for capturing day digits between 01 and 31, inclusively.
418+ * @returns {DateRegExBuilder} builder
419+ * @constructor
420+ */
421+ DD() : DateRegExBuilder {
422+ this.regexString += "(0?[0-9]|1[0-9]|2[0-9]|3[0-1])";
423+ return this;
424+ }
425+
426+ /**
427+ * Add capture group for capturing day digits between 01 and 31, inclusively, in addition to any followed by one or
428+ * more spaces.
429+ * @returns {DateRegExBuilder} builder
430+ * @constructor
431+ */
432+ DD_W_SPACE() : DateRegExBuilder {
433+ this.regexString += "(0?[0-9]|1[0-9]|2[0-9]|3[0-1]|0?[0-9]\\s+|1[0-9]\\s+|2[0-9]\\s+|3[0-1]\\s+)";
434+ return this;
435+ }
436+
437+ /**
438+ * Add capture group for capturing 4 digits or 3 digits starting with 0-9.
439+ * @returns {DateRegExBuilder} builder
440+ * @constructor
441+ */
442+ YYYY() : DateRegExBuilder {
443+ this.regexString += "([0-9]{4}|[1-9][0-9][0-9])";
444+ return this;
445+ }
446+
447+ /**
448+ * Add capture group for capturing 1 through 4 digits.
449+ * @returns {DateRegExBuilder} builder
450+ * @constructor
451+ */
452+ YYYY14() : DateRegExBuilder {
453+ this.regexString += "([0-9]{1,4})";
454+ return this;
455+ }
456+
457+ /**
458+ * Add capture group for capturing 1 through 4 digits, in addition to any followed by one or more spaces.
459+ * @returns {DateRegExBuilder} builder
460+ * @constructor
461+ */
462+ YYYY14_W_SPACE() : DateRegExBuilder {
463+ this.regexString += "([0-9]{1,4}|[0-9]{1,4}\\s+)";
464+ return this;
465+ }
466+
467+ YYYY2_OR_4_W_SPACE() : DateRegExBuilder {
468+ this.regexString += "([0-9]{2}|[0-9]{4}|[0-9]{2}\\s+|[0-9]{4}\\s+)";
469+ return this;
470+ }
471+
472+ /**
473+ * Add capture group for a flexible delimiter, including ", ", " ", ". ", "\", "-".
474+ * @returns {DateRegExBuilder} builder
475+ * @constructor
476+ */
477+ FLEX_DELIMITER() : DateRegExBuilder {
478+ // this.regexString += "(,?\\s+|\\s*-?\\.?-?\\/?\\s+)";// close to being right
479+ this.regexString += "(,?\\s+|\\s*\\.\\s+|\\s*-\\s*|\\s*\\/\\s*)";
480+ return this;
481+ }
482+
483+ /**
484+ * Add capture group for a flexible delimiter, including ", ", " ", ".", "\", "-". Different from FLEX_DELIMITER
485+ * in that it will match periods with zero or more spaces on either side.
486+ * For reference: https://regex101.com/r/q1fp1z/1/
487+ * @returns {DateRegExBuilder} builder
488+ * @constructor
489+ */
490+ FLEX_DELIMITER_LOOSEDOT() : DateRegExBuilder {
491+ // this.regexString += "(,?\\s+|\\s*-?\\.?-?\\/?\\s+)";// close to being right
492+ this.regexString += "(,?\\s+|\\s*\\.\\s*|\\s*-\\s*|\\s*\\/\\s*)";
493+ return this;
494+ }
495+
496+ /**
497+ * Add a capture group for capturing timestamps including: "10am", "10:10", "10:10pm", "10:10:10", "10:10:10am", along
498+ * with zero or more spaces after semi colons, AM or PM, and unlimited number of digits per unit.
499+ * @returns {DateRegExBuilder} builder
500+ * @constructor
501+ */
502+ OPTIONAL_TIMESTAMP_CAPTURE_GROUP() : DateRegExBuilder {
503+ this.regexString += "((\\s+[0-9]+\\s*am\\s*$|[0-9]+\\s*pm\\s*$)|(\\s+[0-9]+:\\s*[0-9]+\\s*$)|(\\s+[0-9]+:\\s*[0-9]+\\s*am\\s*$|\\s+[0-9]+:\\s*[0-9]+\\s*pm\\s*$)|(\\s+[0-9]+:\\s*[0-9]+:\\s*[0-9]+\\s*$)|(\\s+[0-9]+:\\s*[0-9]+:\\s*[0-9]+\\s*am\\s*$|[0-9]+:\\s*[0-9]+:\\s*[0-9]+\\s*pm\\s*$))?";
504+ return this;
505+ }
506+
507+ TIMESTAMP_UNITS_CAPTURE_GROUP() : DateRegExBuilder {
508+ this.regexString += "(\\s*([0-9]+)()()\\s*(am|pm)\\s*$)|(\\s*([0-9]+):\\s*([0-9]+)()()\\s*$)|(\\s*(([0-9]+):\\s*([0-9]+)()\\s*(am|pm))\\s*$)|(\\s*(([0-9]+):\\s*([0-9]+):\\s*([0-9]+)())\\s*$)|(\\s*(([0-9]+):\\s*([0-9]+):\\s*([0-9]+)\\s*(am|pm))\\s*$)";
509+ return this;
510+ }
511+
512+ /**
513+ * Build the regular expression and ignore case.
514+ * @returns {RegExp}
515+ */
516+ build() : RegExp {
517+ return new RegExp(this.regexString, 'i');
518+ }
519+}
520+
521+const YEAR_MONTHDIG_DAY = DateRegExBuilder.DateRegExBuilder()
522+ .start()
523+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY().FLEX_DELIMITER_LOOSEDOT().MM().FLEX_DELIMITER_LOOSEDOT().DD_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
524+ .end()
525+ .build();
526+const MONTHDIG_DAY_YEAR = DateRegExBuilder.DateRegExBuilder()
527+ .start()
528+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MM().FLEX_DELIMITER_LOOSEDOT().DD().FLEX_DELIMITER_LOOSEDOT().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
529+ .end()
530+ .build();
531+const DAY_MONTHNAME_YEAR = DateRegExBuilder.DateRegExBuilder()
532+ .start()
533+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().DD().FLEX_DELIMITER_LOOSEDOT().MONTHNAME().FLEX_DELIMITER_LOOSEDOT().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
534+ .end()
535+ .build();
536+const MONTHNAME_DAY_YEAR = DateRegExBuilder.DateRegExBuilder()
537+ .start()
538+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MONTHNAME().FLEX_DELIMITER_LOOSEDOT().DD().FLEX_DELIMITER_LOOSEDOT().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
539+ .end()
540+ .build();
541+const YEAR_MONTHDIG = DateRegExBuilder.DateRegExBuilder()
542+ .start()
543+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY14().FLEX_DELIMITER().MM_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
544+ .end()
545+ .build();
546+const MONTHDIG_YEAR = DateRegExBuilder.DateRegExBuilder()
547+ .start()
548+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MM().FLEX_DELIMITER().YYYY14_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
549+ .end()
550+ .build();
551+const YEAR_MONTHNAME = DateRegExBuilder.DateRegExBuilder()
552+ .start()
553+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY14().FLEX_DELIMITER().MONTHNAME_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
554+ .end()
555+ .build();
556+const MONTHNAME_YEAR = DateRegExBuilder.DateRegExBuilder()
557+ .start()
558+ .OPTIONAL_DAYNAME().OPTIONAL_COMMA().MONTHNAME().FLEX_DELIMITER().YYYY2_OR_4_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
559+ .end()
560+ .build();
561+// For reference: https://regex101.com/r/47GARA/1/
562+const TIMESTAMP = DateRegExBuilder.DateRegExBuilder()
563+ .start()
564+ .TIMESTAMP_UNITS_CAPTURE_GROUP()
565+ .end()
566+ .build();
567+// The first year to use when calculating the number of days in an ExcelDate
568+const FIRST_YEAR = 1900;
569+// The year 2000.
570+const Y2K_YEAR = 2000;
571+
572
573 /**
574 * Creates a criteria function to evaluate elements in a range in an *IF function.
575@@ -93,6 +341,247 @@ class CriteriaFunctionFactory {
576 * Static class of helpers used to cast various types to each other.
577 */
578 class TypeCaster {
579+
580+ /**
581+ * Casts a string to an ExcelDate. Throws error if parsing not possible.
582+ * @param dateString to parse
583+ * @returns {ExcelDate} resulting date
584+ */
585+ static stringToExcelDate(dateString : string) : ExcelDate {
586+ // m will be set and valid or invalid, or will remain undefined
587+ var m;
588+
589+ /**
590+ * Creates moment object from years, months and days.
591+ * @param years of moment
592+ * @param months of moment in number or string format (eg: January)
593+ * @param days of moment
594+ * @returns {Moment} created moment
595+ */
596+ function createMoment(years, months, days) : moment.Moment {
597+ var actualYear = years;
598+ if (years >= 0 && years < 30) {
599+ actualYear = Y2K_YEAR + years;
600+ } else if (years >= 30 && years < 100) {
601+ actualYear = FIRST_YEAR + years;
602+ }
603+ var tmpMoment = moment.utc([actualYear]).startOf("year");
604+ if (typeof months === "string") {
605+ tmpMoment.month(months);
606+ } else {
607+ tmpMoment.set("months", months);
608+ }
609+ // If we're specifying more days than there are in this month
610+ if (days > tmpMoment.daysInMonth() - 1) {
611+ throw new Error();
612+ }
613+ return tmpMoment.add(days, 'days');
614+ }
615+
616+ /**
617+ * Matches a timestamp string, adding the units to the moment passed in.
618+ * @param timestampString to parse. ok formats: "10am", "10:10", "10:10am", "10:10:10", "10:10:10am", etc.
619+ * @param momentToMutate to mutate
620+ * @returns {Moment} mutated and altered.
621+ */
622+ function matchTimestampAndMutateMoment(timestampString : string, momentToMutate: moment.Moment) : moment.Moment {
623+ var matches = timestampString.match(TIMESTAMP);
624+ if (matches && matches[1] !== undefined) { // 10am
625+ var hours = parseInt(matches[2]);
626+ if (hours > 12) {
627+ throw new Error();
628+ }
629+ // No op on momentToMutate because you can't overload hours with am/pm.
630+ } else if (matches && matches[6] !== undefined) { // 10:10
631+ var hours = parseInt(matches[7]);
632+ var minutes = parseInt(matches[8]);
633+ momentToMutate.add(hours, 'hours').add(minutes, 'minutes');
634+ } else if (matches && matches[11] !== undefined) { // 10:10am
635+ var hours = parseInt(matches[13]);
636+ var minutes = parseInt(matches[14]);
637+ var pmTrue = (matches[16].toLowerCase() === "pm");
638+ if (hours > 12) {
639+ throw new Error();
640+ }
641+ if (pmTrue) {
642+ // 12pm is just 0am, 4pm is 16, etc.
643+ momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
644+ } else {
645+ if (hours !== 12) {
646+ momentToMutate.set('hours', hours);
647+ }
648+ }
649+ momentToMutate.add(minutes, 'minutes');
650+ } else if (matches && matches[17] !== undefined) { // 10:10:10
651+ var hours = parseInt(matches[19]);
652+ var minutes = parseInt(matches[20]);
653+ var seconds = parseInt(matches[21]);
654+ momentToMutate.add(hours, 'hours').add(minutes, 'minutes').add(seconds, 'seconds');
655+ } else if (matches && matches[23] !== undefined) { // // 10:10:10am
656+ var hours = parseInt(matches[25]);
657+ var minutes = parseInt(matches[26]);
658+ var seconds = parseInt(matches[27]);
659+ var pmTrue = (matches[28].toLowerCase() === "pm");
660+ if (hours > 12) {
661+ throw new Error();
662+ }
663+ if (pmTrue) {
664+ // 12pm is just 0am, 4pm is 16, etc.
665+ momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
666+ } else {
667+ if (hours !== 12) {
668+ momentToMutate.set('hours', hours);
669+ }
670+ }
671+ momentToMutate.add(minutes, 'minutes').add(seconds, 'seconds');
672+ } else {
673+ throw new Error();
674+ }
675+ return momentToMutate.set('hours', 0).set('minutes', 0).set('seconds', 0);
676+ }
677+
678+ // Check YEAR_MONTHDIG, YYYY(fd)MM, '1992/06'
679+ // NOTE: Must come before YEAR_MONTHDIG_DAY matching.
680+ if (m === undefined) {
681+ var matches = dateString.match(YEAR_MONTHDIG);
682+ if (matches && matches.length >= 6) {
683+ var years = parseInt(matches[3]);
684+ var months = parseInt(matches[5]) - 1; // Months are zero indexed.
685+ var tmpMoment = createMoment(years, months, 0);
686+ if (matches[6] !== undefined) {
687+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
688+ }
689+ m = tmpMoment;
690+ }
691+ }
692+
693+ // Check YEAR_MONTHDIG_DAY, YYYY(fd)MM(fd)DD, "1992/06/24"
694+ if (m === undefined) {
695+ var matches = dateString.match(YEAR_MONTHDIG_DAY);
696+ if (matches && matches.length >= 8) {
697+ // Check delimiters. If they're not the same, throw error.
698+ if (matches[4].replace(/\s*/g, '') !== matches[6].replace(/\s*/g, '')) {
699+ throw new Error();
700+ }
701+ var years = parseInt(matches[3]);
702+ var months = parseInt(matches[5]) - 1; // Months are zero indexed.
703+ var days = parseInt(matches[7]) - 1; // Days are zero indexed.
704+ var tmpMoment = createMoment(years, months, days);
705+ if (matches.length >= 9 && matches[8] !== undefined) {
706+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
707+ }
708+ m = tmpMoment;
709+ }
710+ }
711+
712+ // Check MONTHDIG_YEAR, MM(fd)YYYY, '06/1992'
713+ // NOTE: Must come before MONTHDIG_DAY_YEAR matching.
714+ if (m === undefined) {
715+ var matches = dateString.match(MONTHDIG_YEAR);
716+ if (matches && matches.length >= 6) {
717+ var years = parseInt(matches[5]);
718+ var months = parseInt(matches[3]) - 1; // Months are zero indexed.
719+ var tmpMoment = createMoment(years, months, 0);
720+ if (matches[6] !== undefined) {
721+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
722+ }
723+ m = tmpMoment;
724+ }
725+ }
726+
727+ // Check MONTHDIG_DAY_YEAR, MM(fd)DD(fd)YYYY, "06/24/1992"
728+ if (m === undefined) {
729+ var matches = dateString.match(MONTHDIG_DAY_YEAR);
730+ if (matches && matches.length >= 8) {
731+ // Check delimiters. If they're not the same, throw error.
732+ if (matches[4].replace(/\s*/g, '') !== matches[6].replace(/\s*/g, '')) {
733+ throw new Error();
734+ }
735+ var years = parseInt(matches[7]);
736+ var months = parseInt(matches[3]) - 1; // Months are zero indexed.
737+ var days = parseInt(matches[5]) - 1; // Days are zero indexed.
738+ var tmpMoment = createMoment(years, months, days);
739+ if (matches.length >= 9 && matches[8] !== undefined) {
740+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
741+ }
742+ m = tmpMoment;
743+ }
744+ }
745+
746+ // Check MONTHNAME_YEAR, Month(fd)YYYY, 'Aug 1992'
747+ // NOTE: Needs to come before DAY_MONTHNAME_YEAR matching.
748+ if (m === undefined) {
749+ var matches = dateString.match(MONTHNAME_YEAR);
750+ if (matches && matches.length >= 6) {
751+ var years = parseInt(matches[5]);
752+ var monthName = matches[3];
753+ var tmpMoment = createMoment(years, monthName, 0);
754+ if (matches[6] !== undefined) {
755+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
756+ }
757+ m = tmpMoment;
758+ }
759+ }
760+
761+ // Check MONTHNAME_DAY_YEAR, Month(fd)DD(fd)YYYY, 'Aug 19 2020'
762+ if (m === undefined) {
763+ var matches = dateString.match(MONTHNAME_DAY_YEAR);
764+ if (matches && matches.length >= 8) {
765+ // Check delimiters. If they're not the same, throw error.
766+ if (matches[4].replace(/\s*/g, '') !== matches[6].replace(/\s*/g, '')) {
767+ throw new Error();
768+ }
769+ var years = parseInt(matches[7]);
770+ var monthName = matches[3];
771+ var days = parseInt(matches[5]) - 1; // Days are zero indexed.
772+ var tmpMoment = createMoment(years, monthName, days);
773+ if (matches.length >= 9 && matches[8] !== undefined) {
774+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
775+ }
776+ m = tmpMoment;
777+ }
778+ }
779+
780+ // Check DAY_MONTHNAME_YEAR, DD(fd)Month(fd)YYYY, '24/July/1992'
781+ if (m === undefined) {
782+ var matches = dateString.match(DAY_MONTHNAME_YEAR);
783+ if (matches && matches.length >= 8) {
784+ var years = parseInt(matches[7]);
785+ var monthName = matches[5];
786+ var days = parseInt(matches[3]) - 1; // Days are zero indexed.
787+ var firstDelimiter = matches[4].replace(/\s*/g, '');
788+ var secondDelimiter = matches[6].replace(/\s*/g, '');
789+ // Check delimiters. If they're not the same, and the first one isn't a space, throw error.
790+ if (firstDelimiter !== secondDelimiter && firstDelimiter !== "") {
791+ throw new Error();
792+ }
793+ var tmpMoment = createMoment(years, monthName, days);
794+ if (matches.length >= 9 && matches[8] !== undefined) {
795+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
796+ }
797+ m = tmpMoment;
798+ }
799+ }
800+
801+ // Check YEAR_MONTHNAME, YYYY(fd)Month, '1992/Aug'
802+ if (m === undefined) {
803+ var matches = dateString.match(YEAR_MONTHNAME);
804+ if (matches && matches.length >= 6) {
805+ var years = parseInt(matches[3]);
806+ var monthName = matches[5];
807+ var tmpMoment = createMoment(years, monthName, 0);
808+ if (matches[6] !== undefined) {
809+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
810+ }
811+ m = tmpMoment;
812+ }
813+ }
814+ if (m === undefined || !m.isValid()) {
815+ throw new CellError(VALUE_ERROR, "DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
816+ }
817+ return new ExcelDate(m);
818+ }
819+
820 /**
821 * Converts any value to a number or throws an error if it cannot coerce it to the number type
822 * @param value to convert
823@@ -267,8 +756,17 @@ class TypeCaster {
824 return value;
825 } else if (typeof value === "number") {
826 return new ExcelDate(value);
827- } else if (typeof value === "string" || typeof value === "boolean") {
828- throw new CellError(ERRORS.VALUE_ERROR, "___ expects boolean values. But '" + value + "' is a text and cannot be coerced to a boolean.")
829+ } else if (typeof value === "string") {
830+ try {
831+ return TypeCaster.stringToExcelDate(value)
832+ } catch (e) {
833+ if (TypeCaster.canCoerceToNumber(value)) {
834+ return new ExcelDate(TypeCaster.valueToNumber(value));
835+ }
836+ throw new CellError(ERRORS.VALUE_ERROR, "___ expects date values. But '" + value + "' is a text and cannot be coerced to a date.")
837+ }
838+ } else if (typeof value === "boolean") {
839+ throw new CellError(ERRORS.VALUE_ERROR, "___ expects date values. But '" + value + "' is a text and cannot be coerced to a date.")
840 }
841 }
842 }
843@@ -400,202 +898,6 @@ class Serializer {
844 }
845
846
847-/**
848- * Build a regular expression step by step, to make it easier to build and read the resulting regular expressions.
849- */
850-class DateRegExBuilder {
851- private regexString = "";
852- private static ZERO_OR_MORE_SPACES = "\\s*";
853-
854- static DateRegExBuilder() : DateRegExBuilder {
855- return new DateRegExBuilder();
856- }
857-
858- /**
859- * Start the regular expression builder by matching the start of a line and zero or more spaces.
860- * @returns {DateRegExBuilder} builder
861- */
862- start() : DateRegExBuilder {
863- this.regexString += "^" + DateRegExBuilder.ZERO_OR_MORE_SPACES;
864- return this;
865- }
866-
867- /**
868- * End the regular expression builder by matching the end of the line.
869- * @returns {DateRegExBuilder} builder
870- */
871- end(): DateRegExBuilder {
872- this.regexString += "$";
873- return this;
874- }
875-
876- /**
877- * Capture all month full name and short names to the regular expression.
878- * @returns {DateRegExBuilder} builder
879- */
880- MONTHNAME() : DateRegExBuilder {
881- this.regexString += "(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)";
882- return this;
883- }
884-
885- /**
886- * Capture all month full name and short names to the regular expression, in addition to any followed by one or more
887- * spaces.
888- * @returns {DateRegExBuilder} builder
889- * @constructor
890- */
891- MONTHNAME_W_SPACE() : DateRegExBuilder {
892- this.regexString += "(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec|january\\s+|february\\s+|march\\s+|april\\s+|may\\s+|june\\s+|july\\s+|august\\s+|september\\s+|october\\s+|november\\s+|december\\s+|jan\\s+|feb\\s+|mar\\s+|apr\\s+|jun\\s+|jul\\s+|aug\\s+|sep\\s+|oct\\s+|nov\\s+|dec\\s+)";
893- return this;
894- }
895-
896- /**
897- * Add capture group for optionally capturing day names.
898- * @returns {DateRegExBuilder} builder
899- * @constructor
900- */
901- OPTIONAL_DAYNAME() : DateRegExBuilder {
902- this.regexString += "(sunday|monday|tuesday|wednesday|thursday|friday|saturday|sun|mon|tue|wed|thu|fri|sat)?";
903- return this;
904- }
905-
906- /**
907- * Add capture group for optionally capturing a comma followed by one or more spaces.
908- * @returns {DateRegExBuilder} builder
909- * @constructor
910- */
911- OPTIONAL_COMMA() : DateRegExBuilder {
912- this.regexString += "(,?\\s+)?";
913- return this;
914- }
915-
916- /**
917- * Add capture group for capturing month digits between 01 and 12, inclusively.
918- * @returns {DateRegExBuilder} builder
919- * @constructor
920- */
921- MM() : DateRegExBuilder {
922- this.regexString += "([1-9]|0[1-9]|1[0-2])";
923- return this;
924- }
925-
926- /**
927- * Add capture group for capturing month digits between 01 and 12, inclusively, in addition to any followed by one or
928- * more spaces.
929- * @returns {DateRegExBuilder} builder
930- * @constructor
931- */
932- MM_W_SPACE() : DateRegExBuilder {
933- this.regexString += "([1-9]|0[1-9]|1[0-2]|[1-9]\\s+|0[1-9]\\s+|1[0-2]\\s+)";
934- return this;
935- }
936-
937- /**
938- * Add capture group for capturing day digits between 01 and 31, inclusively.
939- * @returns {DateRegExBuilder} builder
940- * @constructor
941- */
942- DD() : DateRegExBuilder {
943- this.regexString += "(0?[0-9]|1[0-9]|2[0-9]|3[0-1])";
944- return this;
945- }
946-
947- /**
948- * Add capture group for capturing day digits between 01 and 31, inclusively, in addition to any followed by one or
949- * more spaces.
950- * @returns {DateRegExBuilder} builder
951- * @constructor
952- */
953- DD_W_SPACE() : DateRegExBuilder {
954- this.regexString += "(0?[0-9]|1[0-9]|2[0-9]|3[0-1]|0?[0-9]\\s+|1[0-9]\\s+|2[0-9]\\s+|3[0-1]\\s+)";
955- return this;
956- }
957-
958- /**
959- * Add capture group for capturing 4 digits or 3 digits starting with 0-9.
960- * @returns {DateRegExBuilder} builder
961- * @constructor
962- */
963- YYYY() : DateRegExBuilder {
964- this.regexString += "([0-9]{4}|[1-9][0-9][0-9])";
965- return this;
966- }
967-
968- /**
969- * Add capture group for capturing 1 through 4 digits.
970- * @returns {DateRegExBuilder} builder
971- * @constructor
972- */
973- YYYY14() : DateRegExBuilder {
974- this.regexString += "([0-9]{1,4})";
975- return this;
976- }
977-
978- /**
979- * Add capture group for capturing 1 through 4 digits, in addition to any followed by one or more spaces.
980- * @returns {DateRegExBuilder} builder
981- * @constructor
982- */
983- YYYY14_W_SPACE() : DateRegExBuilder {
984- this.regexString += "([0-9]{1,4}|[0-9]{1,4}\\s+)";
985- return this;
986- }
987-
988- YYYY2_OR_4_W_SPACE() : DateRegExBuilder {
989- this.regexString += "([0-9]{2}|[0-9]{4}|[0-9]{2}\\s+|[0-9]{4}\\s+)";
990- return this;
991- }
992-
993- /**
994- * Add capture group for a flexible delimiter, including ", ", " ", ". ", "\", "-".
995- * @returns {DateRegExBuilder} builder
996- * @constructor
997- */
998- FLEX_DELIMITER() : DateRegExBuilder {
999- // this.regexString += "(,?\\s+|\\s*-?\\.?-?\\/?\\s+)";// close to being right
1000- this.regexString += "(,?\\s+|\\s*\\.\\s+|\\s*-\\s*|\\s*\\/\\s*)";
1001- return this;
1002- }
1003-
1004- /**
1005- * Add capture group for a flexible delimiter, including ", ", " ", ".", "\", "-". Different from FLEX_DELIMITER
1006- * in that it will match periods with zero or more spaces on either side.
1007- * For reference: https://regex101.com/r/q1fp1z/1/
1008- * @returns {DateRegExBuilder} builder
1009- * @constructor
1010- */
1011- FLEX_DELIMITER_LOOSEDOT() : DateRegExBuilder {
1012- // this.regexString += "(,?\\s+|\\s*-?\\.?-?\\/?\\s+)";// close to being right
1013- this.regexString += "(,?\\s+|\\s*\\.\\s*|\\s*-\\s*|\\s*\\/\\s*)";
1014- return this;
1015- }
1016-
1017- /**
1018- * Add a capture group for capturing timestamps including: "10am", "10:10", "10:10pm", "10:10:10", "10:10:10am", along
1019- * with zero or more spaces after semi colons, AM or PM, and unlimited number of digits per unit.
1020- * @returns {DateRegExBuilder} builder
1021- * @constructor
1022- */
1023- OPTIONAL_TIMESTAMP_CAPTURE_GROUP() : DateRegExBuilder {
1024- this.regexString += "((\\s+[0-9]+\\s*am\\s*$|[0-9]+\\s*pm\\s*$)|(\\s+[0-9]+:\\s*[0-9]+\\s*$)|(\\s+[0-9]+:\\s*[0-9]+\\s*am\\s*$|\\s+[0-9]+:\\s*[0-9]+\\s*pm\\s*$)|(\\s+[0-9]+:\\s*[0-9]+:\\s*[0-9]+\\s*$)|(\\s+[0-9]+:\\s*[0-9]+:\\s*[0-9]+\\s*am\\s*$|[0-9]+:\\s*[0-9]+:\\s*[0-9]+\\s*pm\\s*$))?";
1025- return this;
1026- }
1027-
1028- TIMESTAMP_UNITS_CAPTURE_GROUP() : DateRegExBuilder {
1029- this.regexString += "(\\s*([0-9]+)()()\\s*(am|pm)\\s*$)|(\\s*([0-9]+):\\s*([0-9]+)()()\\s*$)|(\\s*(([0-9]+):\\s*([0-9]+)()\\s*(am|pm))\\s*$)|(\\s*(([0-9]+):\\s*([0-9]+):\\s*([0-9]+)())\\s*$)|(\\s*(([0-9]+):\\s*([0-9]+):\\s*([0-9]+)\\s*(am|pm))\\s*$)";
1030- return this;
1031- }
1032-
1033- /**
1034- * Build the regular expression and ignore case.
1035- * @returns {RegExp}
1036- */
1037- build() : RegExp {
1038- return new RegExp(this.regexString, 'i');
1039- }
1040-}
1041-
1042-
1043 export {
1044 ArgsChecker,
1045 CriteriaFunctionFactory,
1046diff --git a/tests/DateFormulasTest.ts b/tests/DateFormulasTest.ts
1047index 6fc08c0..6b6ba10 100644
1048--- a/tests/DateFormulasTest.ts
1049+++ b/tests/DateFormulasTest.ts
1050@@ -23,15 +23,18 @@ function catchAndAssertEquals(toExecute, expected) {
1051 assertEquals(EDATE(DATE(1992, 6, 24), 1), DATE(1992, 7, 24));
1052 assertEquals(EDATE(DATE(1992, 5, 24), 2), DATE(1992, 7, 24));
1053 assertEquals(EDATE(DATE(1992, 5, 24), 2.2), DATE(1992, 7, 24));
1054+assertEquals(EDATE("1992, 5, 24", 2), DATE(1992, 7, 24));
1055+assertEquals(EDATE("6/24/92", 1), DATE(1992, 7, 24));
1056+catchAndAssertEquals(function() {
1057+ EDATE("str", 2);
1058+}, ERRORS.VALUE_ERROR);
1059
1060
1061 // Test DATE
1062 assertEquals(DATE(1900, 1, 2).toNumber(), 3);
1063 assertEquals(DATE(1900, 1, 1).toNumber(), 2);
1064 assertEquals(DATE(1900, 1, 4).toNumber(), 5);
1065-catchAndAssertEquals(function() {
1066- DATE(1900, 0, 4);
1067-}, ERRORS.NUM_ERROR);
1068+
1069 catchAndAssertEquals(function() {
1070 DATE(1900, 0, 5);
1071 }, ERRORS.NUM_ERROR);