spreadsheet
typeScript/javascript spreadsheet parser, with formulas.
git clone https://git.vogt.world/spreadsheet.git
Log | Files | README.md
← Commit log
commit
message
[Date.ts, etc] Another step towards using TypeCaster to to moment<->number conversions
author
Ben Vogt <[email protected]>
date
2017-05-06 00:35:07
stats
3 file(s) changed, 99 insertions(+), 82 deletions(-)
files
src/Formulas/Date.ts
src/Formulas/Financial.ts
src/Utilities/TypeCaster.ts
  1diff --git a/src/Formulas/Date.ts b/src/Formulas/Date.ts
  2index 0c31829..661da12 100644
  3--- a/src/Formulas/Date.ts
  4+++ b/src/Formulas/Date.ts
  5@@ -38,12 +38,12 @@ var DATE = function (...values) : number {
  6     .add(year < FIRST_YEAR ? year : year - FIRST_YEAR, 'years') // If the value is less than 1900, assume 1900 as start index for year
  7     .add(month, 'months')
  8     .add(day, 'days');
  9-  var excelDate = new ExcelDate(m);
 10-  if (excelDate.toNumber() < 0) {
 11-    throw new NumError("DATE evaluates to an out of range value " + excelDate.toNumber()
 12+  var dateAsNumber = TypeCaster.momentToDayNumber(m);
 13+  if (dateAsNumber < 0) {
 14+    throw new NumError("DATE evaluates to an out of range value " + dateAsNumber
 15       + ". It should be greater than or equal to 0.");
 16   }
 17-  return excelDate.toNumber();
 18+  return dateAsNumber;
 19 };
 20 
 21 /**
 22@@ -57,15 +57,15 @@ var DATE = function (...values) : number {
 23 var DATEVALUE = function (...values) : number {
 24   ArgsChecker.checkLength(values, 1);
 25   var dateString = TypeCaster.firstValueAsString(values[0]);
 26-  var date;
 27+  var dateAsNumber;
 28   try {
 29-    date = TypeCaster.stringToExcelDate(dateString);
 30+    dateAsNumber = TypeCaster.stringToExcelDate(dateString);
 31   } catch (e) {
 32     throw new ValueError("DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
 33   }
 34 
 35   // If we've not been able to parse the date by now, then we cannot parse it at all.
 36-  return date.toNumberFloored();
 37+  return dateAsNumber;
 38 };
 39 
 40 
 41@@ -78,14 +78,14 @@ var DATEVALUE = function (...values) : number {
 42  */
 43 var EDATE = function (...values) : number {
 44   ArgsChecker.checkLength(values, 2);
 45-  var startDate = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
 46-  if (startDate.toNumber() < 0) {
 47-    throw new NumError("Function EDATE parameter 1 value is " + startDate.toNumber() + ". It should be greater than or equal to 0.");
 48+  var startDateNumber = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
 49+  if (startDateNumber < 0) {
 50+    throw new NumError("Function EDATE parameter 1 value is " + startDateNumber+ ". It should be greater than or equal to 0.");
 51   }
 52   var months = Math.floor(TypeCaster.firstValueAsNumber(values[1]));
 53   // While ExcelDate.toNumber() will return an inclusive count of days since 1900/1/1, moment.Moment.add assumes
 54   // exclusive count of days.
 55-  return new ExcelDate(moment.utc(ORIGIN_MOMENT).add(startDate.toNumber(), "days").add(months, "months")).toNumber();
 56+  return new ExcelDate(moment.utc(ORIGIN_MOMENT).add(startDateNumber, "days").add(months, "months")).toNumber();
 57 };
 58 
 59 
 60@@ -100,17 +100,17 @@ var EDATE = function (...values) : number {
 61  */
 62 var EOMONTH = function (...values) : number {
 63   ArgsChecker.checkLength(values, 2);
 64-  var startDate = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
 65-  if (startDate.toNumber() < 0) {
 66-    throw new NumError("Function EOMONTH parameter 1 value is " + startDate.toNumber() + ". It should be greater than or equal to 0.");
 67+  var startDateNumber = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
 68+  if (startDateNumber < 0) {
 69+    throw new NumError("Function EOMONTH parameter 1 value is " + startDateNumber + ". It should be greater than or equal to 0.");
 70   }
 71   var months = Math.floor(TypeCaster.firstValueAsNumber(values[1]));
 72   // While ExcelDate.toNumber() will return an inclusive count of days since 1900/1/1, moment.Moment.add assumes
 73   // exclusive count of days.
 74-  return new ExcelDate(moment.utc(ORIGIN_MOMENT)
 75-      .add(startDate.toNumber(), "days")
 76-      .add(months, "months")
 77-      .endOf("month")).toNumberFloored();
 78+  return TypeCaster.momentToDayNumber(moment.utc(ORIGIN_MOMENT)
 79+    .add(startDateNumber, "days")
 80+    .add(months, "months")
 81+    .endOf("month"));
 82 };
 83 
 84 
 85@@ -123,11 +123,11 @@ var EOMONTH = function (...values) : number {
 86  */
 87 var DAY = function (...values) : number {
 88   ArgsChecker.checkLength(values, 1);
 89-  var date = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
 90-  if (date.toNumber() < 0) {
 91-    throw new NumError("Function DAY parameter 1 value is " + date.toNumber() + ". It should be greater than or equal to 0.");
 92+  var dateNumber = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
 93+  if (dateNumber < 0) {
 94+    throw new NumError("Function DAY parameter 1 value is " + dateNumber + ". It should be greater than or equal to 0.");
 95   }
 96-  return date.toMoment().date();
 97+  return TypeCaster.numberToMoment(dateNumber).date();
 98 };
 99 
100 
101@@ -142,7 +142,7 @@ var DAYS = function (...values) : number {
102   ArgsChecker.checkLength(values, 2);
103   var end = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
104   var start = TypeCaster.firstValueAsExcelDate(values[1], true); // tell firstValueAsExcelDate to coerce boolean
105-  return end.toNumber() - start.toNumber();
106+  return end - start;
107 };
108 
109 
110@@ -164,8 +164,8 @@ var DAYS = function (...values) : number {
111  */
112 var DAYS360 = function (...values) : number {
113   ArgsChecker.checkLengthWithin(values, 2, 3);
114-  var start = TypeCaster.firstValueAsExcelDate(values[0], true).toMoment(); // tell firstValueAsExcelDate to coerce boolean
115-  var end = TypeCaster.firstValueAsExcelDate(values[1], true).toMoment(); // tell firstValueAsExcelDate to coerce boolean
116+  var start = TypeCaster.numberToMoment(TypeCaster.firstValueAsExcelDate(values[0], true)); // tell firstValueAsExcelDate to coerce boolean
117+  var end = TypeCaster.numberToMoment(TypeCaster.firstValueAsExcelDate(values[1], true)); // tell firstValueAsExcelDate to coerce boolean
118   var methodToUse = false;
119   if (values.length === 3) {
120     methodToUse = TypeCaster.firstValueAsBoolean(values[2]);
121@@ -204,10 +204,10 @@ var DAYS360 = function (...values) : number {
122 var MONTH = function (...values) : number {
123   ArgsChecker.checkLength(values, 1);
124   var date = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
125-  if (date.toNumber() < 0) {
126-    throw new NumError("Function MONTH parameter 1 value is " + date.toNumber() + ". It should be greater than or equal to 0.");
127+  if (date < 0) {
128+    throw new NumError("Function MONTH parameter 1 value is " + date + ". It should be greater than or equal to 0.");
129   }
130-  return date.toMoment().month() + 1;
131+  return TypeCaster.numberToMoment(date).month() + 1;
132 };
133 
134 
135@@ -221,10 +221,10 @@ var MONTH = function (...values) : number {
136 var YEAR = function (...values) : number {
137   ArgsChecker.checkLength(values, 1);
138   var date = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
139-  if (date.toNumber() < 0) {
140-    throw new NumError("Function YEAR parameter 1 value is " + date.toNumber() + ". It should be greater than or equal to 0.");
141+  if (date < 0) {
142+    throw new NumError("Function YEAR parameter 1 value is " + date + ". It should be greater than or equal to 0.");
143   }
144-  return date.toMoment().year();
145+  return TypeCaster.numberToMoment(date).year();
146 };
147 
148 
149@@ -244,10 +244,10 @@ var WEEKDAY = function (...values) : number {
150   ArgsChecker.checkLengthWithin(values, 1, 2);
151   var date = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
152   var offsetType = values.length === 2 ? TypeCaster.firstValueAsNumber(values[1]) : 1;
153-  if (date.toNumber() < 0) {
154-    throw new NumError("Function WEEKDAY parameter 1 value is " + date.toNumber() + ". It should be greater than or equal to 0.");
155+  if (date < 0) {
156+    throw new NumError("Function WEEKDAY parameter 1 value is " + date + ". It should be greater than or equal to 0.");
157   }
158-  var day = date.toMoment().day();
159+  var day = TypeCaster.numberToMoment(date).day();
160   if (offsetType === 1) {
161     return day + 1;
162   } else if (offsetType === 2) {
163@@ -303,10 +303,10 @@ var WEEKNUM = function (...values) : number {
164   ArgsChecker.checkLengthWithin(values, 1, 2);
165   var date = TypeCaster.firstValueAsExcelDate(values[0], true); // tell firstValueAsExcelDate to coerce boolean
166   var shiftType = values.length === 2 ? TypeCaster.firstValueAsNumber(values[1]) : 1;
167-  if (date.toNumber() < 0) {
168-    throw new NumError("Function YEAR parameter 1 value is " + date.toNumber() + ". It should be greater than or equal to 0.");
169+  if (date < 0) {
170+    throw new NumError("Function YEAR parameter 1 value is " + date + ". It should be greater than or equal to 0.");
171   }
172-  var dm = date.toMoment();
173+  var dm = TypeCaster.numberToMoment(date);
174   var week = dm.week();
175   var dayOfWeek = dm.day(); // between 1 and 7, inclusively
176   if (shiftType === 1) {
177@@ -377,44 +377,43 @@ var DATEDIF = function (...values) : number {
178   var end = TypeCaster.firstValueAsExcelDate(values[1], true);
179   var unit = TypeCaster.firstValueAsString(values[2]);
180   var unitClean = unit.toUpperCase();
181+  var startMoment = TypeCaster.numberToMoment(start);
182+  var endMoment = TypeCaster.numberToMoment(end);
183 
184-  if (start.toNumber() > end.toNumber()) {
185+  if (start > end) {
186     throw new NumError("Function DATEDIF parameter 1 (" + start.toString() +
187       ") should be on or before Function DATEDIF parameter 2 (" + end.toString() + ").");
188   }
189 
190   if (unitClean === "Y") {
191-    return Math.floor(end.toMoment().diff(start.toMoment(), "years"));
192+    return Math.floor(endMoment.diff(startMoment, "years"));
193   } else if (unitClean === "M") {
194-    return Math.floor(end.toMoment().diff(start.toMoment(), "months"));
195+    return Math.floor(endMoment.diff(startMoment, "months"));
196   } else if (unitClean === "D") {
197-    return end.toNumber() - start.toNumber();
198+    return end - start;
199   } else if (unitClean === "MD") {
200-    var s = start.toMoment();
201-    var e = end.toMoment();
202-    while(s.isBefore(e)) {
203+    var s = startMoment;
204+    while(s.isBefore(endMoment)) {
205       s.add(1, "month");
206     }
207     s.subtract(1, "month");
208-    var days = e.diff(s, "days");
209-    return s.date() === e.date() ? 0 : days;
210+    var days = endMoment.diff(s, "days");
211+    return s.date() === endMoment.date() ? 0 : days;
212   } else if (unitClean === "YM") {
213-    var s = start.toMoment();
214-    var e = end.toMoment();
215-    while(s.isBefore(e)) {
216+    var s = startMoment;
217+    while(s.isBefore(endMoment)) {
218       s.add(1, "year");
219     }
220     s.subtract(1, "year");
221-    var months = Math.floor(e.diff(s, "months"));
222+    var months = Math.floor(endMoment.diff(s, "months"));
223     return months === 12 ? 0 : months;
224   } else if (unitClean === "YD") {
225-    var s = start.toMoment();
226-    var e = end.toMoment();
227-    while(s.isBefore(e)) {
228+    var s = startMoment;
229+    while(s.isBefore(endMoment)) {
230       s.add(1, "year");
231     }
232     s.subtract(1, "year");
233-    var days = Math.floor(e.diff(s, "days"));
234+    var days = Math.floor(endMoment.diff(s, "days"));
235     return days >= 365 ? 0 : days;
236   } else {
237     throw new NumError("Function DATEDIF parameter 3 value is " + unit +
238@@ -447,8 +446,8 @@ var YEARFRAC = function (...values) : number {
239   var end = TypeCaster.firstValueAsExcelDate(values[1], true);
240   var basis = values.length === 2 ? 0 : TypeCaster.firstValueAsNumber(values[2]);
241 
242-  var s = start.toMoment();
243-  var e = end.toMoment();
244+  var s = TypeCaster.numberToMoment(start);
245+  var e = TypeCaster.numberToMoment(end);
246   if (e.isBefore(s)) {
247     var me = moment.utc(e);
248     e = moment.utc(s);
249@@ -505,12 +504,12 @@ var YEARFRAC = function (...values) : number {
250         } else if (feb29Between(s, e) || (emonth === 1 && eday === 29)) {
251           ylength = 366;
252         }
253-        return Math.abs((end.toNumber() - start.toNumber()) / ylength);
254+        return Math.abs((end - start) / ylength);
255       } else {
256         var years = (eyear - syear) + 1;
257         var days = moment.utc([eyear+1]).startOf("year").diff(moment.utc([syear]).startOf("year"), 'days');
258         var average = days / years;
259-        return Math.abs((end.toNumber() - start.toNumber()) / average);
260+        return Math.abs((end - start) / average);
261       }
262     // Actual/360
263     case 2:
264@@ -634,16 +633,16 @@ var NETWORKDAYS = function (...values) : number {
265     }
266   }
267   // Handle cases in which the start date is not before the end date.
268-  var didSwap = start.toNumber() > end.toNumber();
269+  var didSwap = start > end;
270   if (didSwap) {
271     var swap = end;
272     end = start;
273     start = swap;
274   }
275 
276-  var c = moment.utc(start.toMoment());
277+  var c = moment.utc(TypeCaster.numberToMoment(start));
278   var weekendDays = [6, 0]; // Default weekend_days.
279-  var days = end.toNumber() - start.toNumber() + 1;
280+  var days = end - start + 1;
281   var networkDays = days;
282   var j = 0;
283   while (j < days) {
284@@ -749,15 +748,15 @@ var NETWORKDAYS$INTL = function (...values) : number {
285     }
286   }
287   // Handle cases in which the start date is not before the end date.
288-  var didSwap = start.toNumber() > end.toNumber();
289+  var didSwap = start > end;
290   if (didSwap) {
291     var swap = end;
292     end = start;
293     start = swap;
294   }
295 
296-  var c = moment.utc(start.toMoment());
297-  var days = end.toNumber() - start.toNumber() + 1;
298+  var c = moment.utc(TypeCaster.numberToMoment(start));
299+  var days = end - start + 1;
300   var networkDays = days;
301   var j = 0;
302   while (j < days) {
303@@ -858,7 +857,7 @@ var WORKDAY = function (...values) : number {
304   }
305 
306   var weekendDays = [0, 6];
307-  var cd = moment.utc(start.toMoment());
308+  var cd = moment.utc(TypeCaster.numberToMoment(start));
309   var j = 0;
310   while (j < days) {
311     cd.add(1, 'days');
312@@ -957,7 +956,7 @@ var WORKDAY$INTL = function (...values) : number {
313       holidays.push(TypeCaster.valueToNumber(values[3]));
314     }
315   }
316-  var cd = moment.utc(start.toMoment());
317+  var cd = moment.utc(TypeCaster.numberToMoment(start));
318   var j = 0;
319   while (j < days) {
320     cd.add(1, 'days');
321diff --git a/src/Formulas/Financial.ts b/src/Formulas/Financial.ts
322index 151fad5..8ef1b4d 100644
323--- a/src/Formulas/Financial.ts
324+++ b/src/Formulas/Financial.ts
325@@ -392,12 +392,12 @@ var ACCRINT = function (...values) {
326   // In MSE, there is a 7th (zero-indexed-6th) param that indicates the calculation-method to use, which indicates
327   // weather the total accrued interest starting at the first_intrest date, instead of the issue date.
328   var firstPayment = TypeCaster.firstValueAsExcelDate(values[1]);
329-  if (firstPayment.toNumber() < 0) {
330-    throw new NumError("Function ACCRINT parameter 2 value is " + firstPayment.toNumber()
331+  if (firstPayment < 0) {
332+    throw new NumError("Function ACCRINT parameter 2 value is " + firstPayment
333         + ". It should be greater than 0.");
334   }
335   var settlement = TypeCaster.firstValueAsExcelDate(values[2]);
336-  if (issue.toNumber() > settlement.toNumber()) {
337+  if (issue > settlement) {
338     throw new NumError("Function ACCRINT parameter 1 (" + issue.toString()
339       + ") should be on or before Function ACCRINT parameter 3 (" + settlement.toString() + ").")
340   }
341diff --git a/src/Utilities/TypeCaster.ts b/src/Utilities/TypeCaster.ts
342index 5b9f1d9..502b837 100644
343--- a/src/Utilities/TypeCaster.ts
344+++ b/src/Utilities/TypeCaster.ts
345@@ -129,6 +129,10 @@ function matchTimestampAndMutateMoment(timestampString : string, momentToMutate:
346  */
347 class TypeCaster {
348 
349+  private static ORIGIN_MOMENT = moment.utc([1899, 11, 30]).startOf("day");
350+  private static SECONDS_IN_DAY = 86400;
351+
352+
353   /**
354    * Converts a time-formatted string to a number between 0 and 1, exclusive on 1.
355    * @param timeString
356@@ -327,13 +331,13 @@ class TypeCaster {
357    * @param dateString to parse
358    * @returns {ExcelDate} resulting date
359    */
360-  public static stringToExcelDate(dateString : string) : ExcelDate {
361+  public static stringToExcelDate(dateString : string) : number {
362     // m will be set and valid or invalid, or will remain undefined
363     var m = TypeCaster.parseStringToMoment(dateString);
364     if (m === undefined || !m.isValid()) {
365       throw new ValueError("DATEVALUE parameter '" + dateString + "' cannot be parsed to date/time.");
366     }
367-    return new ExcelDate(m.set('hours', 0).set('minutes', 0).set('seconds', 0));
368+    return TypeCaster.momentToDayNumber(m.set('hours', 0).set('minutes', 0).set('seconds', 0));
369   }
370 
371   /**
372@@ -515,7 +519,7 @@ class TypeCaster {
373    * @param coerceBoolean should a boolean be converted
374    * @returns {ExcelDate} representing a date
375    */
376-  static firstValueAsExcelDate(input: any, coerceBoolean?: boolean) : ExcelDate {
377+  static firstValueAsExcelDate(input: any, coerceBoolean?: boolean) : number {
378     if (input instanceof Array) {
379       if (input.length === 0) {
380         throw new RefError("Reference does not exist.");
381@@ -541,27 +545,37 @@ class TypeCaster {
382    * @param coerceBoolean should a boolean be converted
383    * @returns {ExcelDate} ExcelDate
384    */
385-  static valueToExcelDate(value: any, coerceBoolean?: boolean) : ExcelDate {
386-    if (value instanceof ExcelDate) {
387+  static valueToExcelDate(value: any, coerceBoolean?: boolean) : number {
388+    if (typeof value === "number") {
389       return value;
390-    } else if (typeof value === "number") {
391-      return ExcelDate.fromDay(value);
392     } else if (typeof value === "string") {
393       try {
394         return TypeCaster.stringToExcelDate(value)
395       } catch (e) {
396         if (TypeCaster.canCoerceToNumber(value)) {
397-          return ExcelDate.fromDay(TypeCaster.valueToNumber(value));
398+          return TypeCaster.valueToNumber(value);
399         }
400         throw new ValueError("___ expects date values. But '" + value + "' is a text and cannot be coerced to a date.")
401       }
402     } else if (typeof value === "boolean") {
403       if (coerceBoolean) {
404-        return ExcelDate.fromDay(value ? 1 : 0);
405+        return value ? 1 : 0;
406       }
407       throw new ValueError("___ expects date values. But '" + value + "' is a boolean and cannot be coerced to a date.")
408     }
409   }
410+
411+  static momentToNumber(m : moment.Moment) : number {
412+    return m.diff(this.ORIGIN_MOMENT, "seconds") / this.SECONDS_IN_DAY;
413+  }
414+
415+  static momentToDayNumber(m : moment.Moment) : number {
416+    return Math.floor(TypeCaster.momentToNumber(m));
417+  }
418+
419+  static numberToMoment(n : number) : moment.Moment {
420+    return moment.utc(TypeCaster.ORIGIN_MOMENT).add(n, "days");
421+  }
422 }
423 
424 /**