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);