spreadsheet
typeScript/javascript spreadsheet parser, with formulas.
git clone https://git.vogt.world/spreadsheet.git
Log | Files | README.md
← Commit log
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);