commit
message
[HOUR, TIMEVALUE] formula added and test, updating parsing and tests for latter
author
Ben Vogt <[email protected]>
date
2017-04-19 01:13:59
stats
4 file(s) changed,
149 insertions(+),
53 deletions(-)
files
src/RawFormulas/Date.ts
src/RawFormulas/RawFormulas.ts
src/RawFormulas/Utils.ts
tests/DateFormulasTest.ts
1diff --git a/src/RawFormulas/Date.ts b/src/RawFormulas/Date.ts
2index 8e83034..eb343b5 100644
3--- a/src/RawFormulas/Date.ts
4+++ b/src/RawFormulas/Date.ts
5@@ -29,13 +29,13 @@ var DATE = function (...values) : ExcelDate {
6 var month = Math.floor(TypeCaster.firstValueAsNumber(values[1])) - 1; // Months are between 0 and 11.
7 var day = Math.floor(TypeCaster.firstValueAsNumber(values[2])) - 1; // Days are also zero-indexed.
8 var m = moment.utc(ORIGIN_DATE).startOf("year")
9- .add(year < FIRST_YEAR ? year : year - FIRST_YEAR, 'years') // If the value is less than 1900, assume 1900 as start index for year
10- .add(month, 'months')
11- .add(day, 'days');
12+ .add(year < FIRST_YEAR ? year : year - FIRST_YEAR, 'years') // If the value is less than 1900, assume 1900 as start index for year
13+ .add(month, 'months')
14+ .add(day, 'days');
15 var excelDate = new ExcelDate(m);
16 if (excelDate.toNumber() < 0) {
17 throw new NumError("DATE evaluates to an out of range value " + excelDate.toNumber()
18- + ". It should be greater than or equal to 0.");
19+ + ". It should be greater than or equal to 0.");
20 }
21 return excelDate;
22 };
23@@ -386,7 +386,7 @@ var DATEDIF = function (...values) : number {
24
25 if (start.toNumber() > end.toNumber()) {
26 throw new NumError("Function DATEDIF parameter 1 (" + start.toString() +
27- ") should be on or before Function DATEDIF parameter 2 (" + end.toString() + ").");
28+ ") should be on or before Function DATEDIF parameter 2 (" + end.toString() + ").");
29 }
30
31 if (unitClean === "Y") {
32@@ -428,7 +428,7 @@ var DATEDIF = function (...values) : number {
33 return days >= 365 ? 0 : days;
34 } else {
35 throw new NumError("Function DATEDIF parameter 3 value is " + unit +
36- ". It should be one of: 'Y', 'M', 'D', 'MD', 'YM', 'YD'.");
37+ ". It should be one of: 'Y', 'M', 'D', 'MD', 'YM', 'YD'.");
38 }
39 };
40
41@@ -549,9 +549,28 @@ var TIMEVALUE = function (...values) : number {
42 }
43 };
44
45+const MILLISECONDS_IN_DAY = 86400000;
46+
47+/**
48+ * Returns the hour component of a specific time, in numeric format.
49+ * @param values[0] time - The time from which to calculate the hour component. Must be a reference to a cell containing
50+ * a date/time, a function returning a date/time type, or a number.
51+ * @returns {number}
52+ * @constructor
53+ */
54+var HOUR = function (...values) : number {
55+ ArgsChecker.checkLength(values, 1);
56+ var time = TypeCaster.firstValueAsTimestampNumber(values[0]);
57+ if (time % 1 === 0) {
58+ return 0;
59+ }
60+ var m = moment.utc([1900]).add(time * MILLISECONDS_IN_DAY, "milliseconds");
61+ return m.hour();
62+};
63+
64+
65
66 // Functions unimplemented.
67-var HOUR;
68 var MINUTE;
69 var NETWORKDAYS;
70 var __COMPLEX_ITL = {
71@@ -578,5 +597,6 @@ export {
72 WEEKDAY,
73 WEEKNUM,
74 YEARFRAC,
75- TIMEVALUE
76+ TIMEVALUE,
77+ HOUR
78 }
79\ No newline at end of file
80diff --git a/src/RawFormulas/RawFormulas.ts b/src/RawFormulas/RawFormulas.ts
81index cbb03d3..053df62 100644
82--- a/src/RawFormulas/RawFormulas.ts
83+++ b/src/RawFormulas/RawFormulas.ts
84@@ -122,7 +122,8 @@ import {
85 WEEKDAY,
86 WEEKNUM,
87 YEARFRAC,
88- TIMEVALUE
89+ TIMEVALUE,
90+ HOUR
91 } from "./Date"
92
93 var ACCRINT = Formula["ACCRINT"];
94@@ -248,5 +249,6 @@ export {
95 WEEKDAY,
96 WEEKNUM,
97 DATEDIF,
98- TIMEVALUE
99+ TIMEVALUE,
100+ HOUR
101 }
102\ No newline at end of file
103diff --git a/src/RawFormulas/Utils.ts b/src/RawFormulas/Utils.ts
104index 9070b01..2ae1225 100644
105--- a/src/RawFormulas/Utils.ts
106+++ b/src/RawFormulas/Utils.ts
107@@ -410,18 +410,25 @@ class TypeCaster {
108 * @returns {number} representing time of day
109 */
110 static stringToTimeNumber(timeString: string) : number {
111- var m = moment.utc([FIRST_YEAR]).startOf("year");
112- m = matchTimestampAndMutateMoment(timeString, m);
113+ var m;
114+ try {
115+ m = matchTimestampAndMutateMoment(timeString, moment.utc([FIRST_YEAR]).startOf("year"));
116+ } catch (e) {
117+ m = TypeCaster.parseStringToMoment(timeString);
118+ if (m === undefined || !m.isValid()) {
119+ throw new Error();
120+ }
121+ }
122+ // If the parsing didn't work, try parsing as timestring alone
123 return (3600 * m.hours() + 60 * m.minutes() + m.seconds()) / 86400;
124 }
125
126 /**
127- * Casts a string to an ExcelDate. Throws error if parsing not possible.
128- * @param dateString to parse
129- * @returns {ExcelDate} resulting date
130+ * Parses a string returning a moment that is either valid, invalid or undefined.
131+ * @param dateString to parse.
132+ * @returns {moment}
133 */
134- static stringToExcelDate(dateString : string) : ExcelDate {
135- // m will be set and valid or invalid, or will remain undefined
136+ private static parseStringToMoment(dateString : string) : moment.Moment {
137 var m;
138
139 /**
140@@ -460,10 +467,7 @@ class TypeCaster {
141 var months = parseInt(matches[5]) - 1; // Months are zero indexed.
142 var tmpMoment = createMoment(years, months, 0);
143 if (matches[6] !== undefined) {
144- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
145- .set('hours', 0)
146- .set('minutes', 0)
147- .set('seconds', 0);
148+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
149 }
150 m = tmpMoment;
151 }
152@@ -482,10 +486,7 @@ class TypeCaster {
153 var days = parseInt(matches[7]) - 1; // Days are zero indexed.
154 var tmpMoment = createMoment(years, months, days);
155 if (matches.length >= 9 && matches[8] !== undefined) {
156- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
157- .set('hours', 0)
158- .set('minutes', 0)
159- .set('seconds', 0);
160+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
161 }
162 m = tmpMoment;
163 }
164@@ -500,10 +501,7 @@ class TypeCaster {
165 var months = parseInt(matches[3]) - 1; // Months are zero indexed.
166 var tmpMoment = createMoment(years, months, 0);
167 if (matches[6] !== undefined) {
168- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
169- .set('hours', 0)
170- .set('minutes', 0)
171- .set('seconds', 0);
172+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
173 }
174 m = tmpMoment;
175 }
176@@ -522,10 +520,7 @@ class TypeCaster {
177 var days = parseInt(matches[5]) - 1; // Days are zero indexed.
178 var tmpMoment = createMoment(years, months, days);
179 if (matches.length >= 9 && matches[8] !== undefined) {
180- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
181- .set('hours', 0)
182- .set('minutes', 0)
183- .set('seconds', 0);
184+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
185 }
186 m = tmpMoment;
187 }
188@@ -540,10 +535,7 @@ class TypeCaster {
189 var monthName = matches[3];
190 var tmpMoment = createMoment(years, monthName, 0);
191 if (matches[6] !== undefined) {
192- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
193- .set('hours', 0)
194- .set('minutes', 0)
195- .set('seconds', 0);
196+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
197 }
198 m = tmpMoment;
199 }
200@@ -562,10 +554,7 @@ class TypeCaster {
201 var days = parseInt(matches[5]) - 1; // Days are zero indexed.
202 var tmpMoment = createMoment(years, monthName, days);
203 if (matches.length >= 9 && matches[8] !== undefined) {
204- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
205- .set('hours', 0)
206- .set('minutes', 0)
207- .set('seconds', 0);
208+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
209 }
210 m = tmpMoment;
211 }
212@@ -586,10 +575,7 @@ class TypeCaster {
213 }
214 var tmpMoment = createMoment(years, monthName, days);
215 if (matches.length >= 9 && matches[8] !== undefined) {
216- tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
217- .set('hours', 0)
218- .set('minutes', 0)
219- .set('seconds', 0);
220+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
221 }
222 m = tmpMoment;
223 }
224@@ -603,18 +589,26 @@ class TypeCaster {
225 var monthName = matches[5];
226 var tmpMoment = createMoment(years, monthName, 0);
227 if (matches[6] !== undefined) {
228- tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
229- .set('hours', 0)
230- .set('minutes', 0)
231- .set('seconds', 0);
232+ tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
233 }
234 m = tmpMoment;
235 }
236 }
237+ return m;
238+ }
239+
240+ /**
241+ * Casts a string to an ExcelDate. Throws error if parsing not possible.
242+ * @param dateString to parse
243+ * @returns {ExcelDate} resulting date
244+ */
245+ public static stringToExcelDate(dateString : string) : ExcelDate {
246+ // m will be set and valid or invalid, or will remain undefined
247+ var m = TypeCaster.parseStringToMoment(dateString);
248 if (m === undefined || !m.isValid()) {
249 throw new ValueError("DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
250 }
251- return new ExcelDate(m);
252+ return new ExcelDate(m.set('hours', 0).set('minutes', 0).set('seconds', 0));
253 }
254
255 /**
256@@ -696,17 +690,20 @@ class TypeCaster {
257 * @param value to convert
258 * @returns {number} representing a time value
259 */
260- static valueAsTimeNumber(value: any) : number {
261+ static valueToTimestampNumber(value: any) : number {
262 if (typeof value === "number") {
263 return value;
264 } else if (typeof value === "string") {
265+ if (value == "") {
266+ return 0;
267+ }
268 try {
269 return TypeCaster.stringToTimeNumber(value)
270 } catch (e) {
271 if (TypeCaster.canCoerceToNumber(value)) {
272 return TypeCaster.valueToNumber(value);
273 }
274- throw new ValueError("___ expects date values. But '" + value + "' is a text and cannot be coerced to a time.")
275+ throw new ValueError("___ expects number values. But '" + value + "' is a text and cannot be coerced to a number.")
276 }
277 } else if (typeof value === "boolean") {
278 return value ? 1 : 0;
279@@ -805,6 +802,16 @@ class TypeCaster {
280 return TypeCaster.valueToExcelDate(input, coerceBoolean);
281 }
282
283+ static firstValueAsTimestampNumber(input : any) : number {
284+ if (input instanceof Array) {
285+ if (input.length === 0) {
286+ throw new RefError("Reference does not exist.");
287+ }
288+ return TypeCaster.firstValueAsTimestampNumber(input[0]);
289+ }
290+ return TypeCaster.valueToTimestampNumber(input);
291+ }
292+
293 /**
294 * Convert a value to ExcelDate if possible.
295 * @param value to convert
296diff --git a/tests/DateFormulasTest.ts b/tests/DateFormulasTest.ts
297index 1a884d5..c831de7 100644
298--- a/tests/DateFormulasTest.ts
299+++ b/tests/DateFormulasTest.ts
300@@ -13,7 +13,8 @@ import {
301 WEEKDAY,
302 WEEKNUM,
303 YEARFRAC,
304- TIMEVALUE
305+ TIMEVALUE,
306+ HOUR
307 } from "../src/RawFormulas/RawFormulas"
308 import * as ERRORS from "../src/Errors"
309 import {assertEquals} from "./utils/Asserts"
310@@ -34,7 +35,63 @@ function catchAndAssertEquals(toExecute, expected) {
311 }
312 }
313
314+
315+// Test HOUR
316+assertEquals(HOUR("8:10"), 8);
317+assertEquals(HOUR("8am"), 8);
318+assertEquals(HOUR("8:10pm"), 20);
319+assertEquals(HOUR("8:10000pm"), 18);
320+assertEquals(HOUR("28:10000"), 2);
321+assertEquals(HOUR("14:23232:9999991"), 10);
322+assertEquals(HOUR(["8:10"]), 8);
323+assertEquals(HOUR("11:21222:2111pm"), 17);
324+assertEquals(HOUR("11:21222:2111am"), 5);
325+assertEquals(HOUR(""), 0);
326+assertEquals(HOUR(0), 0);
327+assertEquals(HOUR(1), 0);
328+assertEquals(HOUR(false), 0);
329+assertEquals(HOUR(true), 0);
330+assertEquals(HOUR(0.8), 19);
331+assertEquals(HOUR(0.5), 12);
332+assertEquals(HOUR(0.25), 6);
333+assertEquals(HOUR(0.125), 3);
334+assertEquals(HOUR(0.0625), 1);
335+assertEquals(HOUR(1.5), 12);
336+assertEquals(HOUR(99.5), 12);
337+assertEquals(HOUR("0.8"), 19);
338+assertEquals(HOUR("0.5"), 12);
339+assertEquals(HOUR("0.25"), 6);
340+assertEquals(HOUR("0.125"), 3);
341+assertEquals(HOUR("0.0625"), 1);
342+assertEquals(HOUR("1969-7-6 5am"), 5);
343+catchAndAssertEquals(function() {
344+ HOUR("8:10", 5);
345+}, ERRORS.NA_ERROR);
346+catchAndAssertEquals(function() {
347+ HOUR();
348+}, ERRORS.NA_ERROR);
349+catchAndAssertEquals(function() {
350+ HOUR("str");
351+}, ERRORS.VALUE_ERROR);
352+catchAndAssertEquals(function() {
353+ HOUR(" ");
354+}, ERRORS.VALUE_ERROR);
355+catchAndAssertEquals(function() {
356+ HOUR([]);
357+}, ERRORS.REF_ERROR);
358+
359+
360 // Test TIMEVALUE
361+assertEquals(TIMEVALUE("1969-7-6"), 0);
362+assertEquals(TIMEVALUE("1969-7-6 8am"), 0.3333333333333333);
363+assertEquals(TIMEVALUE("1969-7-28 8:10"), 0.3402777777777778);
364+assertEquals(TIMEVALUE("2100-7-6 8:10pm"), 0.8402777777777778);
365+assertEquals(TIMEVALUE("1999-1-1 8:10000pm"), 0.7777777777777778);
366+assertEquals(TIMEVALUE("2012/1/1 28:10000"), 0.1111111111111111);
367+assertEquals(TIMEVALUE("2012/1/1 14:23232:9999991"), 0.45730324074074075);
368+assertEquals(TIMEVALUE(["2012/1/1 8:10"]), 0.3402777777777778);
369+assertEquals(TIMEVALUE("2012/1/1 11:21222:2111pm"), 0.7202662037037038);
370+assertEquals(TIMEVALUE("2012/1/1 11:21222:2111am"), 0.2202662037037037);
371 assertEquals(TIMEVALUE("8am"), 0.3333333333333333);
372 assertEquals(TIMEVALUE("8:10"), 0.3402777777777778);
373 assertEquals(TIMEVALUE("8:10pm"), 0.8402777777777778);