spreadsheet
typeScript/javascript spreadsheet parser, with formulas.
git clone https://git.vogt.world/spreadsheet.git
Log | Files | README.md
← Commit log
commit
message
[TIMEVALUE] formula added and tested
author
Ben Vogt <[email protected]>
date
2017-04-16 16:45:11
stats
4 file(s) changed, 203 insertions(+), 109 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 7c6c094..8e83034 100644
  3--- a/src/RawFormulas/Date.ts
  4+++ b/src/RawFormulas/Date.ts
  5@@ -532,6 +532,24 @@ var YEARFRAC = function (...values) : number {
  6 };
  7 
  8 
  9+/**
 10+ * Returns the fraction of a 24-hour day the time represents.
 11+ * @param values[1] time_string - The string that holds the time representation. Eg: "10am", "10:10", "10:10am",
 12+ * "10:10:11", or "10:10:11am".
 13+ * @returns {number} representing the fraction of a 24-hour day
 14+ * @constructor
 15+ */
 16+var TIMEVALUE = function (...values) : number {
 17+  ArgsChecker.checkLength(values, 1);
 18+  var timeString = TypeCaster.firstValueAsString(values[0]);
 19+  try {
 20+    return TypeCaster.stringToTimeNumber(timeString);
 21+  } catch (e) {
 22+    throw new ValueError("TIMEVALUE parameter '" + timeString + "' cannot be parsed to date/time.");
 23+  }
 24+};
 25+
 26+
 27 // Functions unimplemented.
 28 var HOUR;
 29 var MINUTE;
 30@@ -543,7 +561,6 @@ var __COMPLEX_ITL = {
 31 var NOW;
 32 var SECOND;
 33 var TIME;
 34-var TIMEVALUE;
 35 var TODAY;
 36 var WORKDAY;
 37 
 38@@ -560,5 +577,6 @@ export {
 39   YEAR,
 40   WEEKDAY,
 41   WEEKNUM,
 42-  YEARFRAC
 43+  YEARFRAC,
 44+  TIMEVALUE
 45 }
 46\ No newline at end of file
 47diff --git a/src/RawFormulas/RawFormulas.ts b/src/RawFormulas/RawFormulas.ts
 48index 96491c0..cbb03d3 100644
 49--- a/src/RawFormulas/RawFormulas.ts
 50+++ b/src/RawFormulas/RawFormulas.ts
 51@@ -121,7 +121,8 @@ import {
 52   YEAR,
 53   WEEKDAY,
 54   WEEKNUM,
 55-  YEARFRAC
 56+  YEARFRAC,
 57+  TIMEVALUE
 58 } from "./Date"
 59 
 60 var ACCRINT = Formula["ACCRINT"];
 61@@ -246,5 +247,6 @@ export {
 62   YEAR,
 63   WEEKDAY,
 64   WEEKNUM,
 65-  DATEDIF
 66+  DATEDIF,
 67+  TIMEVALUE
 68 }
 69\ No newline at end of file
 70diff --git a/src/RawFormulas/Utils.ts b/src/RawFormulas/Utils.ts
 71index ddda75c..9070b01 100644
 72--- a/src/RawFormulas/Utils.ts
 73+++ b/src/RawFormulas/Utils.ts
 74@@ -335,11 +335,86 @@ class CriteriaFunctionFactory {
 75   }
 76 }
 77 
 78+
 79+/**
 80+ * Matches a timestamp string, adding the units to the moment passed in.
 81+ * @param timestampString to parse. ok formats: "10am", "10:10", "10:10am", "10:10:10", "10:10:10am", etc.
 82+ * @param momentToMutate to mutate
 83+ * @returns {Moment} mutated and altered.
 84+ */
 85+function matchTimestampAndMutateMoment(timestampString : string, momentToMutate: moment.Moment) : moment.Moment {
 86+  var matches = timestampString.match(TIMESTAMP);
 87+  if (matches && matches[1] !== undefined) { // 10am
 88+    var hours = parseInt(matches[2]);
 89+    if (hours > 12) {
 90+      throw new Error();
 91+    }
 92+    momentToMutate.add(hours, 'hours');
 93+  } else if (matches && matches[6] !== undefined) { // 10:10
 94+    var hours = parseInt(matches[7]);
 95+    var minutes = parseInt(matches[8]);
 96+    momentToMutate.add(hours, 'hours').add(minutes, 'minutes');
 97+  } else if (matches && matches[11] !== undefined) { // 10:10am
 98+    var hours = parseInt(matches[13]);
 99+    var minutes = parseInt(matches[14]);
100+    var pmTrue = (matches[16].toLowerCase() === "pm");
101+    if (hours > 12) {
102+      throw new Error();
103+    }
104+    if (pmTrue) {
105+      // 12pm is just 0am, 4pm is 16, etc.
106+      momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
107+    } else {
108+      if (hours !== 12) {
109+        momentToMutate.set('hours', hours);
110+      }
111+    }
112+    momentToMutate.add(minutes, 'minutes');
113+  } else if (matches && matches[17] !== undefined) { // 10:10:10
114+    var hours = parseInt(matches[19]);
115+    var minutes = parseInt(matches[20]);
116+    var seconds = parseInt(matches[21]);
117+    momentToMutate.add(hours, 'hours').add(minutes, 'minutes').add(seconds, 'seconds');
118+  } else if (matches && matches[23] !== undefined) { // // 10:10:10am
119+    var hours = parseInt(matches[25]);
120+    var minutes = parseInt(matches[26]);
121+    var seconds = parseInt(matches[27]);
122+    var pmTrue = (matches[28].toLowerCase() === "pm");
123+    if (hours > 12) {
124+      throw new Error();
125+    }
126+    if (pmTrue) {
127+      // 12pm is just 0am, 4pm is 16, etc.
128+      momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
129+    } else {
130+      if (hours !== 12) {
131+        momentToMutate.set('hours', hours);
132+      }
133+    }
134+    momentToMutate.add(minutes, 'minutes').add(seconds, 'seconds');
135+  } else {
136+    throw new Error();
137+  }
138+  return momentToMutate;
139+}
140+
141+
142 /**
143  * Static class of helpers used to cast various types to each other.
144  */
145 class TypeCaster {
146 
147+  /**
148+   * Converts a time-formatted string to a number between 0 and 1, exclusive on 1.
149+   * @param timeString
150+   * @returns {number} representing time of day
151+   */
152+  static stringToTimeNumber(timeString: string) : number {
153+    var m = moment.utc([FIRST_YEAR]).startOf("year");
154+    m = matchTimestampAndMutateMoment(timeString, m);
155+    return (3600 * m.hours() + 60 * m.minutes() + m.seconds()) / 86400;
156+  }
157+
158   /**
159    * Casts a string to an ExcelDate. Throws error if parsing not possible.
160    * @param dateString to parse
161@@ -376,68 +451,6 @@ class TypeCaster {
162       return tmpMoment.add(days, 'days');
163     }
164 
165-    /**
166-     * Matches a timestamp string, adding the units to the moment passed in.
167-     * @param timestampString to parse. ok formats: "10am", "10:10", "10:10am", "10:10:10", "10:10:10am", etc.
168-     * @param momentToMutate to mutate
169-     * @returns {Moment} mutated and altered.
170-     */
171-    function matchTimestampAndMutateMoment(timestampString : string, momentToMutate: moment.Moment) : moment.Moment {
172-      var matches = timestampString.match(TIMESTAMP);
173-      if (matches && matches[1] !== undefined) { // 10am
174-        var hours = parseInt(matches[2]);
175-        if (hours > 12) {
176-          throw new Error();
177-        }
178-        // No op on momentToMutate because you can't overload hours with am/pm.
179-      } else if (matches && matches[6] !== undefined) { // 10:10
180-        var hours = parseInt(matches[7]);
181-        var minutes = parseInt(matches[8]);
182-        momentToMutate.add(hours, 'hours').add(minutes, 'minutes');
183-      } else if (matches && matches[11] !== undefined) { // 10:10am
184-        var hours = parseInt(matches[13]);
185-        var minutes = parseInt(matches[14]);
186-        var pmTrue = (matches[16].toLowerCase() === "pm");
187-        if (hours > 12) {
188-          throw new Error();
189-        }
190-        if (pmTrue) {
191-          // 12pm is just 0am, 4pm is 16, etc.
192-          momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
193-        } else {
194-          if (hours !== 12) {
195-            momentToMutate.set('hours', hours);
196-          }
197-        }
198-        momentToMutate.add(minutes, 'minutes');
199-      } else if (matches && matches[17] !== undefined) { // 10:10:10
200-        var hours = parseInt(matches[19]);
201-        var minutes = parseInt(matches[20]);
202-        var seconds = parseInt(matches[21]);
203-        momentToMutate.add(hours, 'hours').add(minutes, 'minutes').add(seconds, 'seconds');
204-      } else if (matches && matches[23] !== undefined) { // // 10:10:10am
205-        var hours = parseInt(matches[25]);
206-        var minutes = parseInt(matches[26]);
207-        var seconds = parseInt(matches[27]);
208-        var pmTrue = (matches[28].toLowerCase() === "pm");
209-        if (hours > 12) {
210-          throw new Error();
211-        }
212-        if (pmTrue) {
213-          // 12pm is just 0am, 4pm is 16, etc.
214-          momentToMutate.set('hours', hours === 12 ? hours : 12 + hours);
215-        } else {
216-          if (hours !== 12) {
217-            momentToMutate.set('hours', hours);
218-          }
219-        }
220-        momentToMutate.add(minutes, 'minutes').add(seconds, 'seconds');
221-      } else {
222-        throw new Error();
223-      }
224-      return momentToMutate.set('hours', 0).set('minutes', 0).set('seconds', 0);
225-    }
226-
227     // Check YEAR_MONTHDIG, YYYY(fd)MM, '1992/06'
228     // NOTE: Must come before YEAR_MONTHDIG_DAY matching.
229     if (m === undefined) {
230@@ -447,7 +460,10 @@ class TypeCaster {
231         var months = parseInt(matches[5]) - 1; // Months are zero indexed.
232         var tmpMoment = createMoment(years, months, 0);
233         if (matches[6] !== undefined) {
234-          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
235+          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
236+            .set('hours', 0)
237+            .set('minutes', 0)
238+            .set('seconds', 0);
239         }
240         m = tmpMoment;
241       }
242@@ -466,7 +482,10 @@ class TypeCaster {
243         var days = parseInt(matches[7]) - 1; // Days are zero indexed.
244         var tmpMoment = createMoment(years, months, days);
245         if (matches.length >= 9 && matches[8] !== undefined) {
246-          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
247+          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
248+            .set('hours', 0)
249+            .set('minutes', 0)
250+            .set('seconds', 0);
251         }
252         m = tmpMoment;
253       }
254@@ -481,7 +500,10 @@ class TypeCaster {
255         var months = parseInt(matches[3]) - 1; // Months are zero indexed.
256         var tmpMoment = createMoment(years, months, 0);
257         if (matches[6] !== undefined) {
258-          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
259+          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
260+            .set('hours', 0)
261+            .set('minutes', 0)
262+            .set('seconds', 0);
263         }
264         m = tmpMoment;
265       }
266@@ -500,7 +522,10 @@ class TypeCaster {
267         var days = parseInt(matches[5]) - 1; // Days are zero indexed.
268         var tmpMoment = createMoment(years, months, days);
269         if (matches.length >= 9 && matches[8] !== undefined) {
270-          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
271+          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
272+            .set('hours', 0)
273+            .set('minutes', 0)
274+            .set('seconds', 0);
275         }
276         m = tmpMoment;
277       }
278@@ -515,7 +540,10 @@ class TypeCaster {
279         var monthName = matches[3];
280         var tmpMoment = createMoment(years, monthName, 0);
281         if (matches[6] !== undefined) {
282-          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
283+          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
284+            .set('hours', 0)
285+            .set('minutes', 0)
286+            .set('seconds', 0);
287         }
288         m = tmpMoment;
289       }
290@@ -534,7 +562,10 @@ class TypeCaster {
291         var days = parseInt(matches[5]) - 1; // Days are zero indexed.
292         var tmpMoment = createMoment(years, monthName, days);
293         if (matches.length >= 9 && matches[8] !== undefined) {
294-          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
295+          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
296+            .set('hours', 0)
297+            .set('minutes', 0)
298+            .set('seconds', 0);
299         }
300         m = tmpMoment;
301       }
302@@ -555,7 +586,10 @@ class TypeCaster {
303         }
304         var tmpMoment = createMoment(years, monthName, days);
305         if (matches.length >= 9 && matches[8] !== undefined) {
306-          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
307+          tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment)
308+            .set('hours', 0)
309+            .set('minutes', 0)
310+            .set('seconds', 0);
311         }
312         m = tmpMoment;
313       }
314@@ -569,7 +603,10 @@ class TypeCaster {
315         var monthName = matches[5];
316         var tmpMoment = createMoment(years, monthName, 0);
317         if (matches[6] !== undefined) {
318-          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment);
319+          tmpMoment = matchTimestampAndMutateMoment(matches[6], tmpMoment)
320+            .set('hours', 0)
321+            .set('minutes', 0)
322+            .set('seconds', 0);
323         }
324         m = tmpMoment;
325       }
326@@ -654,6 +691,29 @@ class TypeCaster {
327     }
328   }
329 
330+  /**
331+   * Converts a value to a time number; a value between 0 and 1, exclusive on 1.
332+   * @param value to convert
333+   * @returns {number} representing a time value
334+   */
335+  static valueAsTimeNumber(value: any) : number {
336+    if (typeof value === "number") {
337+      return value;
338+    } else if (typeof value === "string") {
339+      try {
340+        return TypeCaster.stringToTimeNumber(value)
341+      } catch (e) {
342+        if (TypeCaster.canCoerceToNumber(value)) {
343+          return TypeCaster.valueToNumber(value);
344+        }
345+        throw new ValueError("___ expects date values. But '" + value + "' is a text and cannot be coerced to a time.")
346+      }
347+    } else if (typeof value === "boolean") {
348+      return value ? 1 : 0;
349+    }
350+    return 0;
351+  }
352+
353   /**
354    * Returns true if we can coerce it to the number type.
355    * @param value to coerce
356diff --git a/tests/DateFormulasTest.ts b/tests/DateFormulasTest.ts
357index 92f3343..1a884d5 100644
358--- a/tests/DateFormulasTest.ts
359+++ b/tests/DateFormulasTest.ts
360@@ -12,7 +12,8 @@ import {
361   YEAR,
362   WEEKDAY,
363   WEEKNUM,
364-  YEARFRAC
365+  YEARFRAC,
366+  TIMEVALUE
367 } from "../src/RawFormulas/RawFormulas"
368 import * as ERRORS from "../src/Errors"
369 import {assertEquals} from "./utils/Asserts"
370@@ -33,6 +34,30 @@ function catchAndAssertEquals(toExecute, expected) {
371   }
372 }
373 
374+// Test TIMEVALUE
375+assertEquals(TIMEVALUE("8am"), 0.3333333333333333);
376+assertEquals(TIMEVALUE("8:10"), 0.3402777777777778);
377+assertEquals(TIMEVALUE("8:10pm"), 0.8402777777777778);
378+assertEquals(TIMEVALUE("8:10000pm"), 0.7777777777777778);
379+assertEquals(TIMEVALUE("28:10000"), 0.1111111111111111);
380+assertEquals(TIMEVALUE("14:23232:9999991"), 0.45730324074074075);
381+assertEquals(TIMEVALUE(["8:10"]), 0.3402777777777778);
382+assertEquals(TIMEVALUE("11:21222:2111pm"), 0.7202662037037038);
383+assertEquals(TIMEVALUE("11:21222:2111am"), 0.2202662037037037);
384+catchAndAssertEquals(function() {
385+  TIMEVALUE("8:10", 5);
386+}, ERRORS.NA_ERROR);
387+catchAndAssertEquals(function() {
388+  TIMEVALUE();
389+}, ERRORS.NA_ERROR);
390+catchAndAssertEquals(function() {
391+  TIMEVALUE("str");
392+}, ERRORS.VALUE_ERROR);
393+catchAndAssertEquals(function() {
394+  TIMEVALUE([]);
395+}, ERRORS.REF_ERROR);
396+
397+
398 
399 // Test YEARFRAC
400 assertEquals(YEARFRAC("1969-7-6", "1988-7-4", 0), 18.994444444444444);