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