spreadsheet
typeScript/javascript spreadsheet parser, with formulas.
git clone https://git.vogt.world/spreadsheet.git
Log | Files | README.md
← Commit log
commit
message
[TypeConverter] parsing many types of numbers
author
Ben Vogt <[email protected]>
date
2017-05-18 00:55:54
stats
5 file(s) changed, 181 insertions(+), 73 deletions(-)
files
README.md
src/Utilities/TypeConverter.ts
tests/SheetFormulaTest.ts
tests/SheetParseTest.ts
tests/Utilities/TypeConverterTest.ts
  1diff --git a/README.md b/README.md
  2index 4a7a42f..6588f88 100644
  3--- a/README.md
  4+++ b/README.md
  5@@ -20,7 +20,7 @@ Things I should do.
  6 ### Cells should have `formatAs` fields.
  7 Instead of having non-primitives, (i.e. Date, DateTime, Time, Dollar), cells should have formats based on the
  8 highest-order type that was used during the compilation and execution of a cell's dependency. For example, `DATE` might
  9-return a number, but the cell that called `DATE` would be aware of it calling a formula that returns an non-primative
 10+return a number, but the cell that called `DATE` would be aware of it calling a formula that returns an non-primitive
 11 type, and would display the returned number as a Date. If you're using `DATE` in conjunction with `DOLLAR` it would
 12 still display the returned value as a Date. The heirarhchy would look like: [Date, DateTime, Time, Dollar, number,
 13 boolean, string]. Advantages to this would include not having to cast down when using primitive operators,
 14@@ -43,17 +43,6 @@ TypeConverter.
 15 ### Scrape jsdocs for functions, put in simple index.html, doc.md files to serve up simple documentation
 16 
 17 
 18-### Number parsing: `isNumber` or `canCoerceToNumber`, and `valueToNumber` should use a RegEx to determine numbers.
 19-* Numbers that are integers. "10" === 10
 20-* Numbers that are decimals. "10.10" === 10.10
 21-* Numbers with commas in them should still parse to numbers. Eg: "1,000,000" === 1000000
 22-* Numbers with percentages. Eg: "10%" === 0.1, 2 * 1 * 10% === 0.2
 23-* Numbers with scientific notation. Eg: "10e1" === 100
 24-* Numbers with positive or negative notation. Eg: "-10" === -10, "+10" === "10"
 25-* Numbers that are dollar amounts. Eg: "$10.00" === 10
 26-* Numbers that are positive or negative dollar amounts. Eg: "+$10" === 10, "-$10" === 10, "$+10" === 10, "$-10" === 10
 27-
 28-
 29 ### Ensure all formulas are tested inside of SheetFormulaTest.ts
 30 
 31 
 32diff --git a/src/Utilities/TypeConverter.ts b/src/Utilities/TypeConverter.ts
 33index 36310a4..6bb0ca1 100644
 34--- a/src/Utilities/TypeConverter.ts
 35+++ b/src/Utilities/TypeConverter.ts
 36@@ -342,11 +342,82 @@ class TypeConverter {
 37     return TypeConverter.momentToDayNumber(m.set('hours', 0).set('minutes', 0).set('seconds', 0));
 38   }
 39 
 40+  /**
 41+   * Converts strings to numbers, returning undefined if string cannot be parsed to number. Examples: "100", "342424",
 42+   * "10%", "33.213131", "41.1231", "10e1", "10E1", "10.44E1", "-$9.29", "+$9.29", "1,000.1", "2000,000,000".
 43+   * For reference see: https://regex101.com/r/PwghnF/9/
 44+   * @param value to parse.
 45+   * @returns {number} or undefined
 46+   */
 47+  public static stringToNumber(value: string) : number {
 48+    function isUndefined(x) {
 49+      return x === undefined;
 50+    }
 51+    function isDefined(x) {
 52+      return x !== undefined;
 53+    }
 54+
 55+    var NUMBER_REGEX = /^ *(\+|\-)? *(\$)? *(\+|\-)? *((\d+)?(,\d{3})?(,\d{3})?(,\d{3})?(,\d{3})?)? *(\.)? *(\d*)? *(e|E)? *(\d*)? *(%)? *$/;
 56+    var matches = value.match(NUMBER_REGEX);
 57+    if (matches !== null) {
 58+      var firstSign = matches[1];
 59+      var currency = matches[2];
 60+      var secondSign = matches[3];
 61+      var wholeNumberWithCommas = matches[4];
 62+      var decimalPoint = matches[10];
 63+      var decimalNumber = matches[11];
 64+      var sciNotation = matches[12];
 65+      var sciNotationFactor = matches[13];
 66+      var percentageSign = matches[14];
 67+
 68+      // Number is not valid if it is a currency and in scientific notation.
 69+      if (isDefined(currency) && isDefined(sciNotation)) {
 70+        return undefined;
 71+      }
 72+      // Number is not valid if there are two signs.
 73+      if (isDefined(firstSign) && isDefined(secondSign)) {
 74+        return undefined;
 75+      }
 76+      // Number is not valid if we have 'sciNotation' but no 'sciNotationFactor'
 77+      if (isDefined(sciNotation) && isUndefined(sciNotationFactor)) {
 78+        return undefined
 79+      }
 80+      var activeSign;
 81+      if (isUndefined(firstSign) && isUndefined(secondSign)) {
 82+        activeSign = "+";
 83+      } else if (!isUndefined(firstSign)) {
 84+        activeSign = firstSign;
 85+      } else {
 86+        activeSign = secondSign;
 87+      }
 88+      var x;
 89+      if (isDefined(wholeNumberWithCommas)) {
 90+        if (isDefined(decimalNumber) && isDefined(decimalNumber)) {
 91+          // console.log("parsing:", value, activeSign + wholeNumberWithCommas.split(",").join("") + decimalPoint + decimalNumber);
 92+          x = parseFloat(activeSign + wholeNumberWithCommas.split(",").join("") + decimalPoint + decimalNumber);
 93+        } else {
 94+          // console.log("parsing:", value, activeSign + wholeNumberWithCommas.split(",").join(""))
 95+          x = parseFloat(activeSign + wholeNumberWithCommas.split(",").join(""));
 96+        }
 97+      } else {
 98+        // console.log("parsing:", value, activeSign + "0" + decimalPoint + decimalNumber);
 99+        x = parseFloat(activeSign + "0" + decimalPoint + decimalNumber);
100+      }
101+
102+      if (isDefined(sciNotation) && isDefined(sciNotationFactor)) {
103+        x = x * Math.pow(10, parseInt(sciNotationFactor));
104+      }
105+      if (!isUndefined(percentageSign)) {
106+        x = x * 0.01;
107+      }
108+      return x;
109+    }
110+  }
111+
112   /**
113    * Converts any value to a number or throws an error if it cannot coerce it to the number type
114    * @param value to convert
115    * @returns {number} to return. Will always return a number or throw an error. Never returns undefined.
116-   * TODO: This function is far to loosely defined. JS lets anything starting with a digit parse to a number. Not good.
117    */
118   public static valueToNumber(value : any) {
119     if (typeof value === "number") {
120@@ -355,18 +426,11 @@ class TypeConverter {
121       if (value === "") {
122         return 0;
123       }
124-      if (value.indexOf(".") > -1) {
125-        var fl = parseFloat(value.replace("$", ""));
126-        if (isNaN(fl)) {
127-          throw new ValueError("Function ____ expects number values, but is text and cannot be coerced to a number.");
128-        }
129-        return fl;
130-      }
131-      var fl = parseInt(value.replace("$", ""));
132-      if (isNaN(fl)) {
133+      var n = TypeConverter.stringToNumber(value);
134+      if (n === undefined) {
135         throw new ValueError("Function ____ expects number values, but is text and cannot be coerced to a number.");
136       }
137-      return fl;
138+      return n;
139     } else if (typeof value === "boolean") {
140       return value ? 1 : 0;
141     }
142@@ -441,23 +505,25 @@ class TypeConverter {
143     return 0;
144   }
145 
146+  /**
147+   * Returns true if string is number format.
148+   * @param str to check
149+   * @returns {boolean}
150+   */
151+  public static isNumber(str : string) {
152+    return str.match("\s*(\d+\.?\d*$)|(\.\d+$)|([0-9]{2}%$)|([0-9]{1,}$)") !== null;
153+  }
154+
155   /**
156    * Returns true if we can coerce it to the number type.
157    * @param value to coerce
158    * @returns {boolean} if could be coerced to a number
159-   TODO: Similar to valueToNumber, JS lets anything starting with a digit parse to a number.
160    */
161   public static canCoerceToNumber(value: any) : boolean {
162     if (typeof value === "number" || typeof value === "boolean") {
163       return true;
164     } else if (typeof value === "string") {
165-      if (value === "") {
166-        return false;
167-      }
168-      if (value.indexOf(".") > -1) {
169-        return !isNaN(parseFloat(value));
170-      }
171-      return !isNaN(parseInt(value));
172+      return TypeConverter.isNumber(value);
173     }
174     return false;
175   }
176diff --git a/tests/SheetFormulaTest.ts b/tests/SheetFormulaTest.ts
177index 1ded579..60a38a6 100644
178--- a/tests/SheetFormulaTest.ts
179+++ b/tests/SheetFormulaTest.ts
180@@ -453,11 +453,18 @@ test("Sheet ^", function(){
181   assertFormulaEquals('= 2 ^ 10', 1024);
182 });
183 
184-test("Sheet mathMathc", function(){
185+test("Sheet numbers/math", function(){
186   assertFormulaEquals('= "10" + 10', 20);
187-  // TODO: throws parse error, should clean up
188-  // assertFormulaEqualsError('= 10e / 1', VALUE_ERROR);
189-  // TODO: Should fail, but doesn't because 10e parses to a string
190-  // assertFormulaEqualsError('= "10e" + 10', VALUE_ERROR);
191-  assertFormulaEqualsError('= "MR" + 10', VALUE_ERROR);
192-});
193\ No newline at end of file
194+  assertFormulaEquals('="10.111111" + 0', 10.111111);
195+  assertFormulaEquals('= 10%', 0.1);
196+  assertFormulaEquals('= 10% + 1', 1.1);
197+  // TODO: These fail
198+  // assertFormulaEquals('="10e1" + 0', 100);
199+  // assertFormulaEquals('="1,000,000" + 0', 1000000);
200+  // assertFormulaEqualsError('= "10e" + 10', VALUE_ERROR); // TODO: Should fail, but doesn't because 10e parses to a string
201+  // assertFormulaEquals('="+$10.00" + 0', 10);
202+  // assertFormulaEquals('="-$10.00" + 0', -10);
203+  // assertFormulaEquals('="$+10.00" + 0', 10);
204+  // assertFormulaEquals('="$-10.00" + 0', -10);
205+});
206+
207diff --git a/tests/SheetParseTest.ts b/tests/SheetParseTest.ts
208deleted file mode 100644
209index 60a4238..0000000
210--- a/tests/SheetParseTest.ts
211+++ /dev/null
212@@ -1,21 +0,0 @@
213-import {
214-  Sheet
215-} from "../src/Sheet";
216-import {
217-  assertEquals,
218-  test
219-} from "./Utils/Asserts";
220-
221-
222-test("Sheet parsing math formulas", function(){
223-  var sheet  = new Sheet();
224-  sheet.setCell("A1", "=10 * 10");
225-  var cell = sheet.getCell("A1");
226-  assertEquals(100, cell.getValue());
227-
228-  var sheet  = new Sheet();
229-  sheet.setCell("A1", "=SUM(10) + 12");
230-  var cell = sheet.getCell("A1");
231-  assertEquals(22, cell.getValue());
232-});
233-
234diff --git a/tests/Utilities/TypeConverterTest.ts b/tests/Utilities/TypeConverterTest.ts
235index 3d3e831..130595e 100644
236--- a/tests/Utilities/TypeConverterTest.ts
237+++ b/tests/Utilities/TypeConverterTest.ts
238@@ -174,6 +174,72 @@ test("TypeConverter.valueToNumber", function () {
239 });
240 
241 
242+test("TypeConverter.stringToNumber", function () {
243+  assertEquals(TypeConverter.stringToNumber("10"), 10);
244+  assertEquals(TypeConverter.stringToNumber("-10"), -10);
245+  assertEquals(TypeConverter.stringToNumber("1.4832749823"), 1.4832749823);
246+  assertEquals(TypeConverter.stringToNumber("   1.4832749823   "), 1.4832749823);
247+  assertEquals(TypeConverter.stringToNumber("$10"), 10);
248+  assertEquals(TypeConverter.stringToNumber("$10.217983172"), 10.217983172);
249+  assertEquals(TypeConverter.stringToNumber("-$10.217983172"), -10.217983172);
250+  assertEquals(TypeConverter.stringToNumber("100"), 100);
251+  assertEquals(TypeConverter.stringToNumber("10%"), 0.1);
252+  assertEquals(TypeConverter.stringToNumber("33.213131"), 33.213131);
253+  assertEquals(TypeConverter.stringToNumber("41.1231"), 41.1231);
254+  assertEquals(TypeConverter.stringToNumber("10e1"), 100);
255+  assertEquals(TypeConverter.stringToNumber("10E1"), 100);
256+  assertEquals(TypeConverter.stringToNumber("10.44E1"), 104.39999999999999);
257+  assertEquals(TypeConverter.stringToNumber("10.44E10"), 104400000000);
258+  assertEquals(TypeConverter.stringToNumber("$10"), 10);
259+  assertEquals(TypeConverter.stringToNumber("$0.1"), 0.1);
260+  assertEquals(TypeConverter.stringToNumber("$10.1"), 10.1);
261+  assertEquals(TypeConverter.stringToNumber("$9.2222"), 9.2222);
262+  assertEquals(TypeConverter.stringToNumber("+$9.2345"), 9.2345);
263+  assertEquals(TypeConverter.stringToNumber("+$  9.29"), 9.29);
264+  assertEquals(TypeConverter.stringToNumber("+$ 9.29"), 9.29);
265+  assertEquals(TypeConverter.stringToNumber("+$9.2345"), 9.2345);
266+  assertEquals(TypeConverter.stringToNumber("+$  9.29"), 9.29);
267+  assertEquals(TypeConverter.stringToNumber("+$ 9.29"), 9.29);
268+  assertEquals(TypeConverter.stringToNumber("$.1"), 0.1);
269+  assertEquals(TypeConverter.stringToNumber("+$.111"), 0.111);
270+  assertEquals(TypeConverter.stringToNumber("$+.111"), 0.111);
271+  assertEquals(TypeConverter.stringToNumber("-$.1"), -0.1);
272+  assertEquals(TypeConverter.stringToNumber("$-9.2345"), -9.2345);
273+  assertEquals(TypeConverter.stringToNumber("$ - 9.29"), -9.29);
274+  assertEquals(TypeConverter.stringToNumber("$- 9.29"), -9.29);
275+  assertEquals(TypeConverter.stringToNumber("-$9.2345"), -9.2345);
276+  assertEquals(TypeConverter.stringToNumber("-$  9.29"), -9.29);
277+  assertEquals(TypeConverter.stringToNumber("-$ 9.29"), -9.29);
278+  assertEquals(TypeConverter.stringToNumber("-$9"), -9);
279+  assertEquals(TypeConverter.stringToNumber("+$9"), 9);
280+  assertEquals(TypeConverter.stringToNumber("$-9"), -9);
281+  assertEquals(TypeConverter.stringToNumber("$+9"), 9);
282+  assertEquals(TypeConverter.stringToNumber("-$9."), -9);
283+  assertEquals(TypeConverter.stringToNumber("+$9."), 9);
284+  assertEquals(TypeConverter.stringToNumber("$-9."), -9);
285+  assertEquals(TypeConverter.stringToNumber("$+9."), 9);
286+  assertEquals(TypeConverter.stringToNumber("1,000"), 1000);
287+  assertEquals(TypeConverter.stringToNumber("1,000,000"), 1000000);
288+  assertEquals(TypeConverter.stringToNumber("1000,000"), 1000000);
289+  assertEquals(TypeConverter.stringToNumber("2222,000,000"), 2222000000);
290+  assertEquals(TypeConverter.stringToNumber("1,000.1"), 1000.1);
291+  assertEquals(TypeConverter.stringToNumber("1,000,000.11"), 1000000.11);
292+  assertEquals(TypeConverter.stringToNumber("2222,000,000.1"), 2222000000.1);
293+  assertEquals(TypeConverter.stringToNumber(" $ 1,000"), 1000);
294+  assertEquals(TypeConverter.stringToNumber("$ 1,000"), 1000);
295+  assertEquals(TypeConverter.stringToNumber("100.1e2"), 10010);
296+  assertEquals(TypeConverter.stringToNumber("10e2%"), 10);
297+  assertEquals(TypeConverter.stringToNumber("$ 1,000."), 1000);
298+  assertEquals(TypeConverter.stringToNumber("$10e1"), undefined);
299+  assertEquals(TypeConverter.stringToNumber("$+-10.00"), undefined);
300+  assertEquals(TypeConverter.stringToNumber("+$+10.00"), undefined);
301+  assertEquals(TypeConverter.stringToNumber("+$-10.00"), undefined);
302+  assertEquals(TypeConverter.stringToNumber("10e"), undefined);
303+  assertEquals(TypeConverter.stringToNumber("10,00"), undefined);
304+  assertEquals(TypeConverter.stringToNumber("10,000,"), undefined);
305+});
306+
307+
308 test("TypeConverter.valueToNumberGracefully", function () {
309   assertEquals(TypeConverter.valueToNumberGracefully(10), 10);
310   assertEquals(TypeConverter.valueToNumberGracefully(-10), -10);