spreadsheet
typeScript/javascript spreadsheet parser, with formulas.
git clone https://git.vogt.world/spreadsheet.git
Log | Files | README.md
← Commit log
commit
message
[Parser, Sheet, MoreUtils] refactoring static utility functions
author
Ben Vogt <[email protected]>
date
2017-12-20 00:47:44
stats
4 file(s) changed, 281 insertions(+), 209 deletions(-)
files
src/Parser/Parser.ts
src/Sheet.ts
src/Utilities/MoreUtils.ts
src/Utilities/TypeConverter.ts
  1diff --git a/src/Parser/Parser.ts b/src/Parser/Parser.ts
  2index 6c9494e..1f720ca 100644
  3--- a/src/Parser/Parser.ts
  4+++ b/src/Parser/Parser.ts
  5@@ -1,5 +1,5 @@
  6 import {
  7-  constructErrorByName,
  8+  constructErrorByName, DivZeroError,
  9   ParseError
 10 } from "../Errors";
 11 import {
 12@@ -19,7 +19,118 @@ import {
 13   RuleIndex,
 14   Symbol
 15 } from "./ParserConstants"
 16-import {isUndefined} from "../Utilities/MoreUtils";
 17+import {
 18+  isArray,
 19+  isUndefined,
 20+  string
 21+} from "../Utilities/MoreUtils";
 22+import {TypeConverter} from "../Utilities/TypeConverter";
 23+
 24+
 25+/**
 26+ * Performs logical operations on two values.
 27+ * @param type of logic operation
 28+ * @param exp1
 29+ * @param exp2
 30+ * @returns {boolean}
 31+ */
 32+function logicMatch(type, exp1, exp2) {
 33+  let result;
 34+
 35+  switch (type) {
 36+    case '=':
 37+      result = (exp1 === exp2);
 38+      break;
 39+
 40+    case '>':
 41+      result = (exp1 > exp2);
 42+      break;
 43+
 44+    case '<':
 45+      result = (exp1 < exp2);
 46+      break;
 47+
 48+    case '>=':
 49+      result = (exp1 >= exp2);
 50+      break;
 51+
 52+    case '<=':
 53+      result = (exp1 <= exp2);
 54+      break;
 55+
 56+    case '<>':
 57+      result = (exp1 != exp2);
 58+      break;
 59+
 60+    case 'NOT':
 61+      result = (exp1 != exp2);
 62+      break;
 63+  }
 64+
 65+  return result;
 66+}
 67+
 68+/**
 69+ * Performs math operations on two values.
 70+ * @param type
 71+ * @param number1
 72+ * @param number2
 73+ * @returns {number}
 74+ */
 75+function mathMatch(type, number1, number2) {
 76+  let result;
 77+
 78+  number1 = TypeConverter.valueToNumber(number1);
 79+  number2 = TypeConverter.valueToNumber(number2);
 80+
 81+  switch (type) {
 82+    case '+':
 83+      result = number1 + number2;
 84+      break;
 85+    case '-':
 86+      result = number1 - number2;
 87+      break;
 88+    case '/':
 89+      if (number2 === 0) {
 90+        throw new DivZeroError("Evaluation caused divide by zero error.");
 91+      }
 92+      if (number2 !== 0 && number1 === 0) {
 93+        result = 0;
 94+      }
 95+      result = number1 / number2;
 96+      if (result == Infinity) {
 97+        throw new DivZeroError("Evaluation caused divide by zero error.");
 98+      } else if (isNaN(result)) {
 99+        throw new DivZeroError("Evaluation caused divide by zero error.");
100+      }
101+      break;
102+    case '*':
103+      result = number1 * number2;
104+      break;
105+    case '^':
106+      result = Math.pow(number1, number2);
107+      break;
108+  }
109+  return result;
110+}
111+
112+/**
113+ * Performs special operations on two values. Currently only concatenation.
114+ * @param type
115+ * @param exp1
116+ * @param exp2
117+ * @returns {any}
118+ */
119+function specialMatch(type, exp1, exp2) {
120+  let result;
121+
122+  switch (type) {
123+    case '&':
124+      result = exp1.toString() + exp2.toString();
125+      break;
126+  }
127+  return result;
128+}
129 
130 let Parser = (function () {
131   let parser = {
132@@ -51,58 +162,58 @@ let Parser = (function () {
133             this.$ = sharedStateYY.handler.callVariable.call(this, virtualStack[vsl]);
134             break;
135           case ReduceActions.AS_NUMBER:
136-            this.$ = sharedStateYY.handler.number(virtualStack[vsl]);
137+            this.$ = TypeConverter.valueToNumber(virtualStack[vsl]);
138             break;
139           case ReduceActions.AS_STRING:
140-            this.$ = sharedStateYY.handler.string(virtualStack[vsl]);
141+            this.$ = string(virtualStack[vsl]);
142             break;
143           case ReduceActions.AMPERSAND:
144-            this.$ = sharedStateYY.handler.specialMatch('&', virtualStack[vsl - 2], virtualStack[vsl]);
145+            this.$ = specialMatch('&', virtualStack[vsl - 2], virtualStack[vsl]);
146             break;
147           case ReduceActions.EQUALS:
148-            this.$ = sharedStateYY.handler.logicMatch('=', virtualStack[vsl - 2], virtualStack[vsl]);
149+            this.$ = logicMatch('=', virtualStack[vsl - 2], virtualStack[vsl]);
150             break;
151           case ReduceActions.PLUS:
152-            this.$ = sharedStateYY.handler.mathMatch('+', virtualStack[vsl - 2], virtualStack[vsl]);
153+            this.$ = mathMatch('+', virtualStack[vsl - 2], virtualStack[vsl]);
154             break;
155           case ReduceActions.LAST_NUMBER:
156-            this.$ = sharedStateYY.handler.number(virtualStack[vsl - 1]);
157+            this.$ = TypeConverter.valueToNumber(virtualStack[vsl - 1]);
158             break;
159           case ReduceActions.LTE:
160-            this.$ = sharedStateYY.handler.logicMatch('<=', virtualStack[vsl - 3], virtualStack[vsl]);
161+            this.$ = logicMatch('<=', virtualStack[vsl - 3], virtualStack[vsl]);
162             break;
163           case ReduceActions.GTE:
164-            this.$ = sharedStateYY.handler.logicMatch('>=', virtualStack[vsl - 3], virtualStack[vsl]);
165+            this.$ = logicMatch('>=', virtualStack[vsl - 3], virtualStack[vsl]);
166             break;
167           case ReduceActions.NOT_EQ:
168-            this.$ = sharedStateYY.handler.logicMatch('<>', virtualStack[vsl - 3], virtualStack[vsl]);
169+            this.$ = logicMatch('<>', virtualStack[vsl - 3], virtualStack[vsl]);
170             break;
171           case ReduceActions.GT:
172-            this.$ = sharedStateYY.handler.logicMatch('>', virtualStack[vsl - 2], virtualStack[vsl]);
173+            this.$ = logicMatch('>', virtualStack[vsl - 2], virtualStack[vsl]);
174             break;
175           case ReduceActions.LT:
176-            this.$ = sharedStateYY.handler.logicMatch('<', virtualStack[vsl - 2], virtualStack[vsl]);
177+            this.$ = logicMatch('<', virtualStack[vsl - 2], virtualStack[vsl]);
178             break;
179           case ReduceActions.MINUS:
180-            this.$ = sharedStateYY.handler.mathMatch('-', virtualStack[vsl - 2], virtualStack[vsl]);
181+            this.$ = mathMatch('-', virtualStack[vsl - 2], virtualStack[vsl]);
182             break;
183           case ReduceActions.MULTIPLY:
184-            this.$ = sharedStateYY.handler.mathMatch('*', virtualStack[vsl - 2], virtualStack[vsl]);
185+            this.$ = mathMatch('*', virtualStack[vsl - 2], virtualStack[vsl]);
186             break;
187           case ReduceActions.DIVIDE:
188-            this.$ = sharedStateYY.handler.mathMatch('/', virtualStack[vsl - 2], virtualStack[vsl]);
189+            this.$ = mathMatch('/', virtualStack[vsl - 2], virtualStack[vsl]);
190             break;
191           case ReduceActions.TO_POWER:
192-            this.$ = sharedStateYY.handler.mathMatch('^', virtualStack[vsl - 2], virtualStack[vsl]);
193+            this.$ = mathMatch('^', virtualStack[vsl - 2], virtualStack[vsl]);
194             break;
195           case ReduceActions.INVERT_NUM:
196-            this.$ = sharedStateYY.handler.numberInverted(virtualStack[vsl]);
197+            this.$ = TypeConverter.valueToInvertedNumber(virtualStack[vsl]);
198             if (isNaN(this.$)) {
199               this.$ = 0;
200             }
201             break;
202           case ReduceActions.TO_NUMBER_NAN_AS_ZERO:
203-            this.$ = sharedStateYY.handler.number(virtualStack[vsl]);
204+            this.$ = TypeConverter.valueToNumber(virtualStack[vsl]);
205             if (isNaN(this.$)) {
206               this.$ = 0;
207             }
208@@ -126,7 +237,7 @@ let Parser = (function () {
209             this.$ = sharedStateYY.handler.cellRangeValue(sharedStateYY.obj, virtualStack[vsl - 2], virtualStack[vsl]);
210             break;
211           case ReduceActions.ENSURE_IS_ARRAY:
212-            if (sharedStateYY.handler.isArray(virtualStack[vsl])) {
213+            if (isArray(virtualStack[vsl])) {
214               this.$ = virtualStack[vsl];
215             } else {
216               this.$ = [virtualStack[vsl]];
217@@ -149,7 +260,7 @@ let Parser = (function () {
218             this.$ = [virtualStack[vsl]];
219             break;
220           case ReduceActions.ENSURE_LAST_TWO_IN_ARRAY_AND_PUSH:
221-            this.$ = (sharedStateYY.handler.isArray(virtualStack[vsl - 2]) ? virtualStack[vsl - 2] : [virtualStack[vsl - 2]]);
222+            this.$ = (isArray(virtualStack[vsl - 2]) ? virtualStack[vsl - 2] : [virtualStack[vsl - 2]]);
223             this.$.push(virtualStack[vsl]);
224             break;
225           case ReduceActions.REFLEXIVE_REDUCE:
226@@ -212,7 +323,7 @@ let Parser = (function () {
227               }
228               break;
229             case ReduceActions.ENSURE_IS_ARRAY:
230-              if (sharedStateYY.handler.isArray(virtualStack[vsl])) {
231+              if (isArray(virtualStack[vsl])) {
232                 this.$ = virtualStack[vsl];
233               } else {
234                 this.$ = [virtualStack[vsl]];
235@@ -235,7 +346,7 @@ let Parser = (function () {
236               this.$ = [virtualStack[vsl]];
237               break;
238             case ReduceActions.ENSURE_LAST_TWO_IN_ARRAY_AND_PUSH:
239-              this.$ = (sharedStateYY.handler.isArray(virtualStack[vsl - 2]) ? virtualStack[vsl - 2] : [virtualStack[vsl - 2]]);
240+              this.$ = (isArray(virtualStack[vsl - 2]) ? virtualStack[vsl - 2] : [virtualStack[vsl - 2]]);
241               this.$.push(virtualStack[vsl]);
242               break;
243             case ReduceActions.REFLEXIVE_REDUCE:
244diff --git a/src/Sheet.ts b/src/Sheet.ts
245index e7c37e9..98168dc 100644
246--- a/src/Sheet.ts
247+++ b/src/Sheet.ts
248@@ -1,9 +1,12 @@
249 import {Cell} from "./Cell";
250-import {DivZeroError, NameError, RefError} from "./Errors";
251+import {NameError, RefError} from "./Errors";
252 import {DataStore} from "./Parser/DataStore";
253 import {FormulaParser} from "./Parser/Parser";
254-import {TypeConverter} from "./Utilities/TypeConverter";
255 import {Formulas} from "./Formulas";
256+import {
257+  isFunction, characterToNumber, numberToCharacter, convertXYtoA1Coordinates,
258+  A1toRowColCoordinates
259+} from "./Utilities/MoreUtils";
260 
261 // TODO: Document.
262 class Sheet {
263@@ -21,64 +24,6 @@ class Sheet {
264     this.dataStore = new DataStore();
265   }
266 
267-  isArray (value) {
268-    return value instanceof Array;
269-  };
270-
271-  isFunction (value) {
272-    return value instanceof Function;
273-  };
274-
275-  toNum (chr) {
276-    chr = this.clearFormula(chr);
277-    let base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', i, j, result = 0;
278-
279-    for (i = 0, j = chr.length - 1; i < chr.length; i += 1, j -= 1) {
280-      result += Math.pow(base.length, j) * (base.indexOf(chr[i]) + 1);
281-    }
282-
283-    if (result) {
284-      --result;
285-    }
286-
287-    return result;
288-  };
289-
290-  toChar (num) {
291-    let s = '';
292-
293-    while (num >= 0) {
294-      s = String.fromCharCode(num % 26 + 97) + s;
295-      num = Math.floor(num / 26) - 1;
296-    }
297-
298-    return s.toUpperCase();
299-  };
300-
301-  XYtoA1 (x, y) {
302-    function numberToLetters(num) {
303-      let mod = num % 26,
304-        pow = num / 26 | 0,
305-        out = mod ? String.fromCharCode(64 + mod) : (--pow, 'Z');
306-      return pow ? numberToLetters(pow) + out : out;
307-    }
308-    return numberToLetters(x+1) + (y+1).toString();
309-  };
310-
311-  cellCoords (cell) {
312-    let num = cell.match(/\d+$/),
313-      alpha = cell.replace(num, '');
314-
315-    return {
316-      row: parseInt(num[0], 10) - 1,
317-      col: this.toNum(alpha)
318-    };
319-  };
320-
321-  clearFormula (formula) {
322-    return formula.replace(/\$/g, '');
323-  };
324-
325   iterateCells (origin, startCell, endCell, callback?) {
326     let result = {
327       index: [], // list of cell index: A1, A2, A3
328@@ -121,7 +66,7 @@ class Sheet {
329 
330     for (let column = cols.start; column <= cols.end; column++) {
331       for (let row = rows.start; row <= rows.end; row++) {
332-        let cellIndex = this.toChar(column) + (row + 1),
333+        let cellIndex = numberToCharacter(column) + (row + 1),
334           cellValue = this.cellValue(origin, cellIndex);
335 
336         result.index.push(cellIndex);
337@@ -129,117 +74,13 @@ class Sheet {
338       }
339     }
340 
341-    if (this.isFunction(callback)) {
342+    if (isFunction(callback)) {
343       return callback.apply(callback, [result]);
344     } else {
345       return result;
346     }
347   }
348 
349-  sort(rev) {
350-    return function (a, b) {
351-      return ((a < b) ? -1 : ((a > b) ? 1 : 0)) * (rev ? -1 : 1);
352-    }
353-  }
354-
355-
356-  number(x) {
357-    return TypeConverter.valueToNumber(x);
358-  }
359-
360-  string(str) {
361-    return str.substring(1, str.length - 1);
362-  }
363-
364-  numberInverted (num) {
365-    return this.number(num) * (-1);
366-  }
367-
368-  specialMatch(type, exp1, exp2) {
369-    let result;
370-
371-    switch (type) {
372-      case '&':
373-        result = exp1.toString() + exp2.toString();
374-        break;
375-    }
376-    return result;
377-  }
378-
379-  logicMatch(type, exp1, exp2) {
380-    let result;
381-
382-    switch (type) {
383-      case '=':
384-        result = (exp1 === exp2);
385-        break;
386-
387-      case '>':
388-        result = (exp1 > exp2);
389-        break;
390-
391-      case '<':
392-        result = (exp1 < exp2);
393-        break;
394-
395-      case '>=':
396-        result = (exp1 >= exp2);
397-        break;
398-
399-      case '<=':
400-        result = (exp1 <= exp2);
401-        break;
402-
403-      case '<>':
404-        result = (exp1 != exp2);
405-        break;
406-
407-      case 'NOT':
408-        result = (exp1 != exp2);
409-        break;
410-    }
411-
412-    return result;
413-  };
414-
415-  mathMatch(type, number1, number2) {
416-    let result;
417-
418-    number1 = this.number(number1);
419-    number2 = this.number(number2);
420-
421-    switch (type) {
422-      case '+':
423-        result = number1 + number2;
424-        break;
425-      case '-':
426-        result = number1 - number2;
427-        break;
428-      case '/':
429-        if (number2 === 0) {
430-          throw new DivZeroError("Evaluation caused divide by zero error.");
431-        }
432-        if (number2 !== 0 && number1 === 0) {
433-          result = 0;
434-        }
435-        result = number1 / number2;
436-        if (result == Infinity) {
437-          throw new DivZeroError("Evaluation caused divide by zero error.");
438-        } else if (isNaN(result)) {
439-          throw new DivZeroError("Evaluation caused divide by zero error.");
440-        }
441-        break;
442-      case '*':
443-        result = number1 * number2;
444-        break;
445-      case '^':
446-        result = Math.pow(number1, number2);
447-        break;
448-    }
449-
450-    return result;
451-  }
452-
453   callFunction(fn, args) {
454     fn = fn.toUpperCase();
455     args = args || [];
456@@ -279,8 +120,8 @@ class Sheet {
457   }
458 
459   cellRangeValue(origin, start: string, end: string) {
460-    let coordsStart = this.cellCoords(start),
461-      coordsEnd = this.cellCoords(end);
462+    let coordsStart = A1toRowColCoordinates(start),
463+      coordsEnd = A1toRowColCoordinates(end);
464 
465     // iterate cells to get values and indexes
466     let cells = this.iterateCells(origin, coordsStart, coordsEnd),
467@@ -384,7 +225,7 @@ class Sheet {
468     for (let y = 0; y < input.length; y++) {
469       for (let x = 0; x < input[0].length; x++) {
470         // set the cell here
471-        let id = this.XYtoA1(x, y);
472+        let id = convertXYtoA1Coordinates(x, y);
473         this.setCell(id, input[y][x].toString());
474       }
475     }
476diff --git a/src/Utilities/MoreUtils.ts b/src/Utilities/MoreUtils.ts
477index cb0e2b4..a5bb31f 100644
478--- a/src/Utilities/MoreUtils.ts
479+++ b/src/Utilities/MoreUtils.ts
480@@ -16,6 +16,100 @@ function isDefined(value : any) : boolean {
481   return value !== undefined;
482 }
483 
484+/**
485+ * Returns true if value is an instance of a Array.
486+ * @param value
487+ * @returns {boolean}
488+ */
489+function isArray(value) : boolean {
490+  return value instanceof Array;
491+}
492+
493+/**
494+ * Returns true if value is instance of a Function.
495+ * @param value
496+ * @returns {boolean}
497+ */
498+function isFunction(value) : boolean {
499+  return value instanceof Function;
500+}
501+
502+/**
503+ * Alphabetical character to number.
504+ * @param chr
505+ * @returns {number}
506+ */
507+function characterToNumber(chr) {
508+  chr = chr.replace(/\$/g, '');
509+  let base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', i, j, result = 0;
510+
511+  for (i = 0, j = chr.length - 1; i < chr.length; i += 1, j -= 1) {
512+    result += Math.pow(base.length, j) * (base.indexOf(chr[i]) + 1);
513+  }
514+
515+  if (result) {
516+    --result;
517+  }
518+
519+  return result;
520+}
521+
522+/**
523+ * Converts a number to an alphabetical character.
524+ * @param num
525+ * @returns {string}
526+ */
527+function numberToCharacter(num) {
528+  let s = '';
529+
530+  while (num >= 0) {
531+    s = String.fromCharCode(num % 26 + 97) + s;
532+    num = Math.floor(num / 26) - 1;
533+  }
534+
535+  return s.toUpperCase();
536+}
537+
538+/**
539+ * Converts a quoted string to an un-quoted string: `"hey"` to `hey`
540+ * @param str
541+ * @returns {string}
542+ */
543+function string(str){
544+  return str.substring(1, str.length - 1);
545+}
546+
547+
548+/**
549+ * Converts XY coordinates (eg {0, 0}) to A1 coordinates (eg {A1}).
550+ * @param x
551+ * @param y
552+ * @returns {string}
553+ */
554+function convertXYtoA1Coordinates(x, y) {
555+  function numberToLetters(num) {
556+    let mod = num % 26,
557+      pow = num / 26 | 0,
558+      out = mod ? String.fromCharCode(64 + mod) : (--pow, 'Z');
559+    return pow ? numberToLetters(pow) + out : out;
560+  }
561+  return numberToLetters(x+1) + (y+1).toString();
562+}
563+
564+/**
565+ * Returns RowCol coordinates of an A1 cellId
566+ * @param cellId
567+ * @returns {Object}
568+ */
569+function A1toRowColCoordinates(cellId) {
570+  let num = cellId.match(/\d+$/),
571+    alpha = cellId.replace(num, '');
572+
573+  return {
574+    row: parseInt(num[0], 10) - 1,
575+    col: characterToNumber(alpha)
576+  };
577+}
578 
579 /**
580  * Class for building formatted strings with commas, forced number of leading and trailing zeros, and arbitrary leading
581@@ -181,7 +275,6 @@ class NumberStringBuilder {
582       }
583     }
584 
585-
586     // Inserting commas if necessary.
587     if (this.shouldUseComma) {
588       integerPart = integerPart.split("").reverse().map(function (digit, index) {
589@@ -211,5 +304,12 @@ class NumberStringBuilder {
590 export {
591   isDefined,
592   isUndefined,
593+  isArray,
594+  isFunction,
595+  string,
596+  numberToCharacter,
597+  convertXYtoA1Coordinates,
598+  A1toRowColCoordinates,
599+  characterToNumber,
600   NumberStringBuilder
601 }
602\ No newline at end of file
603diff --git a/src/Utilities/TypeConverter.ts b/src/Utilities/TypeConverter.ts
604index 0875701..9d79c02 100644
605--- a/src/Utilities/TypeConverter.ts
606+++ b/src/Utilities/TypeConverter.ts
607@@ -457,6 +457,15 @@ class TypeConverter {
608     }
609   }
610 
611+  /**
612+   * Converts any value to an inverted number or throws an error if it cannot coerce it to the number type
613+   * @param value to convert
614+   * @returns {number} to return. Will always return a number or throw an error. Never returns undefined.
615+   */
616+  public static valueToInvertedNumber(value: any) {
617+    return TypeConverter.valueToNumber(value) * (-1);
618+  }
619+
620   /**
621    * Converts any value to a number or throws an error if it cannot coerce it to the number type
622    * @param value to convert