commit
message
[Text.TEXT] formula added and tested
author
Ben Vogt <[email protected]>
date
2017-09-24 17:30:42
stats
18 file(s) changed,
1264 insertions(+),
33 deletions(-)
files
DOCS.md
TODO.md
dist/Formulas/AllFormulas.js
dist/Formulas/Text.js
dist/Utilities/MoreUtils.js
dist/Utilities/TypeConverter.js
package.json
src/Formulas/AllFormulas.ts
src/Formulas/Text.ts
src/Utilities/MoreUtils.ts
src/Utilities/TypeConverter.ts
tests/Formulas/DateFormulasTestTimeOverride.ts
tests/Formulas/TextTest.ts
tests/SheetFormulaTest.ts
tests/Utilities/MoreUtilsTest.ts
tests/Utilities/TypeConverterTest.ts
tests/Utils/Asserts.ts
tsconfig.json
1diff --git a/DOCS.md b/DOCS.md
2index e8ff30e..253cf2a 100644
3--- a/DOCS.md
4+++ b/DOCS.md
5@@ -1554,6 +1554,18 @@
6 @returns {number}
7 @constructor
8 ```
9+
10+### SERIESSUM
11+
12+```
13+ Returns a sum of powers of the number x in accordance with the following formula.
14+@param x - The number as an independent variable.
15+@param n - The starting power.
16+@param m - The number to increment by
17+@param coefficients - A series of coefficients. For each coefficient the series sum is extended by one section. You can only enter coefficients using cell references.
18+@returns {number}
19+@constructor
20+```
21 ## Range
22
23
24@@ -2296,3 +2308,29 @@
25 @param value - Value to return.
26 @constructor
27 ```
28+
29+### ROMAN
30+
31+```
32+ Converts a number into a Roman numeral.
33+@param value - The value to convert. Must be between 0 and 3999.
34+@constructor TODO: Second parameter should be 'rule_relaxation'.
35+```
36+
37+### TEXT
38+
39+```
40+ Converts a number into text according to a given format.
41+@param value - The value to be converted.
42+@param format - Text which defines the format. "0" forces the display of zeros, while "#" suppresses the display of zeros. For example TEXT(22.1,"000.00") produces 022.10, while TEXT(22.1,"###.##") produces 22.1, and TEXT(22.405,"00.00") results in 22.41. To format days: "dddd" indicates full name of the day of the week, "ddd" hort name of the day of the week, "dd" indicates the day of the month as two digits, "d" indicates day of the month as one or two digits, "mmmmm" indicates the first letter in the month of the year, "mmmm" indicates the full name of the month of the year, "mmm" indicates short name of the month of the year, "mm" indicates month of the year as two digits or the number of minutes in a time, depending on whether it follows yy or dd, or if it follows hh, "m" month of the year as one or two digits or the number of minutes in a time, depending on whether it follows yy or dd, or if it follows hh, "yyyy" indicates year as four digits, "yy" and "y" indicate year as two digits, "hh" indicates hour on a 24-hour clock, "h" indicates hour on a 12-hour clock, "ss.000" indicates milliseconds in a time, "ss" indicates econds in a time, "AM/PM" or "A/P" indicate displaying hours based on a 12-hour clock and showing AM or PM depending on the time of day. Eg: `TEXT("01/09/2012 10:04:33AM", "mmmm-dd-yyyy, hh:mm AM/PM")` would result in "January-09-2012, 10:04 AM".
43+@constructor
44+```
45+
46+###
47+
48+```
49+ Converts a number into text according to a given format.
50+@param value - The value to be converted.
51+@param format - Text which defines the format. "0" forces the display of zeros, while "#" suppresses the display of zeros. For example TEXT(22.1,"000.00") produces 022.10, while TEXT(22.1,"###.##") produces 22.1, and TEXT(22.405,"00.00") results in 22.41. To format days: "dddd" indicates full name of the day of the week, "ddd" hort name of the day of the week, "dd" indicates the day of the month as two digits, "d" indicates day of the month as one or two digits, "mmmmm" indicates the first letter in the month of the year, "mmmm" indicates the full name of the month of the year, "mmm" indicates short name of the month of the year, "mm" indicates month of the year as two digits or the number of minutes in a time, depending on whether it follows yy or dd, or if it follows hh, "m" month of the year as one or two digits or the number of minutes in a time, depending on whether it follows yy or dd, or if it follows hh, "yyyy" indicates year as four digits, "yy" and "y" indicate year as two digits, "hh" indicates hour on a 24-hour clock, "h" indicates hour on a 12-hour clock, "ss.000" indicates milliseconds in a time, "ss" indicates econds in a time, "AM/PM" or "A/P" indicate displaying hours based on a 12-hour clock and showing AM or PM depending on the time of day. Eg: `TEXT("01/09/2012 10:04:33AM", "mmmm-dd-yyyy, hh:mm AM/PM")` would result in "January-09-2012, 10:04 AM".
52+@constructor if (format.match(/^.(d|D|M|m|yy|Y|HH|hh|h|s|S|AM|PM|am|pm|A\/P|\).$/g)) { const POUND_SIGN_FORMAT_CAPTURE = /^([$
53+```
54diff --git a/TODO.md b/TODO.md
55index e4cb46c..f02b1e7 100644
56--- a/TODO.md
57+++ b/TODO.md
58@@ -16,6 +16,10 @@ Currently, this `=SERIESSUM([1], [0], [1], [4, 5, 6])` parses, but this `=SERIES
59 ### Parser/Sheet should be able to be initialized with js range notation (`[]`) or regular range notation (`{}`)
60
61
62+### TypeConverter.stringToDateNumber should handle fractions of a second.
63+E.g. `01/09/2012 10:04:33.123`
64+
65+
66 ### Parser should be able to parse arrays without `eval`
67 Right now, arrays and reference literals in a formula are parsed using JS `eval`. This means, if we have references inside, or non-JS parsing values like TRUE or FALSE, they will cause ReferenceErrors. For example, `=SUM([M1, 10])` would throw `[ReferenceError: M1 is not defined]` because M1 is not a variable. Instead of using `eval`, we should parse the opening of an array, and the closeing of an array, and use recursion to see how deep we are, evaluating the tokens inside in the sam way we parse formulas and functions.
68
69@@ -63,7 +67,6 @@ Many of these formulas can be written by allowing the Sheet and Parser to return
70 * SEARCH
71 * SEARCHB
72 * SUBSTITUTE
73-* TEXT
74 * VALUE
75 * LOGEST
76 * MDETERM
77diff --git a/dist/Formulas/AllFormulas.js b/dist/Formulas/AllFormulas.js
78index 62311d9..174b51e 100644
79--- a/dist/Formulas/AllFormulas.js
80+++ b/dist/Formulas/AllFormulas.js
81@@ -220,6 +220,7 @@ exports.LOWER = Text_1.LOWER;
82 exports.UPPER = Text_1.UPPER;
83 exports.T = Text_1.T;
84 exports.ROMAN = Text_1.ROMAN;
85+exports.TEXT = Text_1.TEXT;
86 var Date_1 = require("./Date");
87 exports.DATE = Date_1.DATE;
88 exports.DATEVALUE = Date_1.DATEVALUE;
89diff --git a/dist/Formulas/Text.js b/dist/Formulas/Text.js
90index 428c66d..790ba46 100644
91--- a/dist/Formulas/Text.js
92+++ b/dist/Formulas/Text.js
93@@ -3,6 +3,8 @@ exports.__esModule = true;
94 var ArgsChecker_1 = require("../Utilities/ArgsChecker");
95 var TypeConverter_1 = require("../Utilities/TypeConverter");
96 var Errors_1 = require("../Errors");
97+var Filter_1 = require("../Utilities/Filter");
98+var MoreUtils_1 = require("../Utilities/MoreUtils");
99 /**
100 * Computes the value of a Roman numeral.
101 * @param text - The Roman numeral to format, whose value must be between 1 and 3999, inclusive.
102@@ -397,7 +399,6 @@ var CONVERT = function (value, startUnit, endUnit) {
103 }
104 // Return error if a unit does not exist
105 if (from === null || to === null) {
106- console.log(from, to);
107 throw new Errors_1.NAError("Invalid units for conversion.");
108 }
109 // Return error if units represent different quantities
110@@ -460,7 +461,7 @@ exports.T = T;
111 * Converts a number into a Roman numeral.
112 * @param value - The value to convert. Must be between 0 and 3999.
113 * @constructor
114- * TODO: Second parameter should be 'rule_relaxation'
115+ * TODO: Second parameter should be 'rule_relaxation'.
116 */
117 var ROMAN = function (value) {
118 ArgsChecker_1.ArgsChecker.checkLength(arguments, 1, "ROMAN");
119@@ -471,6 +472,7 @@ var ROMAN = function (value) {
120 }
121 // The MIT License
122 // Copyright (c) 2008 Steven Levithan
123+ // https://stackoverflow.com/questions/9083037/convert-a-number-into-a-roman-numeral-in-javascript
124 var digits = String(value).split('');
125 var key = ['',
126 'C',
127@@ -511,3 +513,193 @@ var ROMAN = function (value) {
128 return new Array(+digits.join('') + 1).join('M') + roman;
129 };
130 exports.ROMAN = ROMAN;
131+/**
132+ * Converts a number into text according to a given format.
133+ * @param value - The value to be converted.
134+ * @param format - Text which defines the format. "0" forces the display of zeros, while "#" suppresses the display of
135+ * zeros. For example TEXT(22.1,"000.00") produces 022.10, while TEXT(22.1,"###.##") produces 22.1, and
136+ * TEXT(22.405,"00.00") results in 22.41. To format days: "dddd" indicates full name of the day of the week, "ddd"
137+ * short name of the day of the week, "dd" indicates the day of the month as two digits, "d" indicates day of the month
138+ * as one or two digits, "mmmmm" indicates the first letter in the month of the year, "mmmm" indicates the full name of
139+ * the month of the year, "mmm" indicates short name of the month of the year, "mm" indicates month of the year as two
140+ * digits or the number of minutes in a time, depending on whether it follows yy or dd, or if it follows hh, "m" month
141+ * of the year as one or two digits or the number of minutes in a time, depending on whether it follows yy or dd, or if
142+ * it follows hh, "yyyy" indicates year as four digits, "yy" and "y" indicate year as two digits, "hh" indicates hour
143+ * on a 24-hour clock, "h" indicates hour on a 12-hour clock, "ss.000" indicates milliseconds in a time, "ss" indicates
144+ * seconds in a time, "AM/PM" or "A/P" indicate displaying hours based on a 12-hour clock and showing AM or PM
145+ * depending on the time of day. Eg: `TEXT("01/09/2012 10:04:33AM", "mmmm-dd-yyyy, hh:mm AM/PM")` would result in
146+ * "January-09-2012, 10:04 AM".
147+ * @constructor
148+ */
149+var TEXT = function (value, format) {
150+ ArgsChecker_1.ArgsChecker.checkLength(arguments, 2, "TEXT");
151+ value = TypeConverter_1.TypeConverter.firstValue(value);
152+ function splitReplace(values, regex, index) {
153+ return values.map(function (value) {
154+ if (typeof value === "number") {
155+ return [value];
156+ }
157+ else if (value instanceof Array) {
158+ return splitReplace(value, regex, index);
159+ }
160+ else {
161+ var splits_1 = value.split(regex);
162+ var building_1 = [];
163+ if (splits_1.length === 1) {
164+ return [splits_1];
165+ }
166+ splits_1.map(function (splitValue, splitIndex) {
167+ building_1.push(splitValue);
168+ if (splitIndex !== splits_1.length - 1) {
169+ building_1.push(index);
170+ }
171+ });
172+ return building_1;
173+ }
174+ });
175+ }
176+ // Short cut for booleans
177+ if (typeof value === "boolean") {
178+ return TypeConverter_1.TypeConverter.valueToString(value);
179+ }
180+ // If the format matches the date format
181+ if (format.match(/^.*(d|D|M|m|yy|Y|HH|hh|h|s|S|AM|PM|am|pm|A\/P|\*).*$/g)) {
182+ // If the format contains both, throw error
183+ if (format.indexOf("#") > -1 || format.indexOf("0") > -1) {
184+ throw new Errors_1.ValueError("Invalid format pattern '" + format + "' for TEXT formula.");
185+ }
186+ var valueAsMoment_1;
187+ if (typeof value === "string") {
188+ valueAsMoment_1 = TypeConverter_1.TypeConverter.stringToMoment(value);
189+ if (valueAsMoment_1 === undefined) {
190+ valueAsMoment_1 = TypeConverter_1.TypeConverter.decimalNumberToMoment(TypeConverter_1.TypeConverter.valueToNumber(value));
191+ }
192+ }
193+ else {
194+ valueAsMoment_1 = TypeConverter_1.TypeConverter.decimalNumberToMoment(TypeConverter_1.TypeConverter.valueToNumber(value));
195+ }
196+ var replacementPairs_1 = [
197+ // full name of the day of the week
198+ [/dddd/gi, valueAsMoment_1.format("dddd")],
199+ // short name of the day of the week
200+ [/ddd/gi, valueAsMoment_1.format("ddd")],
201+ // day of the month as two digits
202+ [/dd/gi, valueAsMoment_1.format("DD")],
203+ // day of the month as one or two digits
204+ [/d/gi, valueAsMoment_1.format("d")],
205+ // first letter in the month of the year
206+ [/mmmmm/gi, valueAsMoment_1.format("MMMM").charAt(0)],
207+ // full name of the month of the year
208+ [/mmmm/gi, valueAsMoment_1.format("MMMM")],
209+ // short name of the month of the year
210+ [/mmm/gi, valueAsMoment_1.format("MMM")],
211+ // month of the year as two digits or the number of minutes in a time
212+ [/mm/gi, function (monthOrMinute) {
213+ return monthOrMinute === "month" ? valueAsMoment_1.format("MM") : valueAsMoment_1.format("mm");
214+ }],
215+ // month of the year as one or two digits or the number of minutes in a time
216+ [/m/g, function (monthOrMinute) {
217+ return monthOrMinute === "month" ? valueAsMoment_1.format("M") : valueAsMoment_1.format("m");
218+ }],
219+ // year as four digits
220+ [/yyyy/gi, valueAsMoment_1.format("YYYY")],
221+ // year as two digits
222+ [/yy/gi, valueAsMoment_1.format("YY")],
223+ // year as two digits
224+ [/y/gi, valueAsMoment_1.format("YY")],
225+ // hour on a 24-hour clock
226+ [/HH/g, valueAsMoment_1.format("HH")],
227+ // hour on a 12-hour clock
228+ [/hh/g, valueAsMoment_1.format("hh")],
229+ // hour on a 12-hour clock
230+ [/h/gi, valueAsMoment_1.format("hh")],
231+ // milliseconds in a time
232+ [/ss\.000/gi, valueAsMoment_1.format("ss.SSS")],
233+ // seconds in a time
234+ [/ss/gi, valueAsMoment_1.format("ss")],
235+ // seconds in a time
236+ [/s/gi, valueAsMoment_1.format("ss")],
237+ [/AM\/PM/gi, valueAsMoment_1.format("A")],
238+ // displaying hours based on a 12-hour clock and showing AM or PM depending on the time of day
239+ [/A\/P/gi, valueAsMoment_1.format("A").charAt(0)]
240+ ];
241+ var builtList_1 = [format];
242+ replacementPairs_1.map(function (pair, pairIndex) {
243+ var regex = pair[0];
244+ builtList_1 = splitReplace(builtList_1, regex, pairIndex);
245+ });
246+ var lastRegEx_1 = "";
247+ return Filter_1.Filter.flatten(builtList_1).map(function (val) {
248+ if (typeof val === "number") {
249+ if (typeof replacementPairs_1[val][1] === "function") {
250+ var monthOrMinute = "month";
251+ // Hack-ish way of determining if MM, mm, M, or m should be evaluated as minute or month.
252+ var lastRegExWasHour = lastRegEx_1.toString() === new RegExp("hh", "g").toString()
253+ || lastRegEx_1.toString() === new RegExp("HH", "g").toString()
254+ || lastRegEx_1.toString() === new RegExp("h", "g").toString();
255+ if (lastRegExWasHour) {
256+ monthOrMinute = "minute";
257+ }
258+ lastRegEx_1 = replacementPairs_1[val][0];
259+ return replacementPairs_1[val][1](monthOrMinute);
260+ }
261+ lastRegEx_1 = replacementPairs_1[val][0];
262+ return replacementPairs_1[val][1];
263+ }
264+ return val;
265+ }).join("");
266+ }
267+ else {
268+ var numberValue = TypeConverter_1.TypeConverter.valueToNumber(value);
269+ // Format string can't contain both 0 and #.
270+ if (format.indexOf("#") > -1 && format.indexOf("0") > -1) {
271+ throw new Errors_1.ValueError("Invalid format pattern '" + format + "' for TEXT formula.");
272+ }
273+ // See https://regex101.com/r/Jji2Ng/8 for more information.
274+ var POUND_SIGN_FORMAT_CAPTURE = /^([$%+-]*)([#,]+)?(\.?)([# ]*)([$%+ -]*)$/gi;
275+ var matches = POUND_SIGN_FORMAT_CAPTURE.exec(format);
276+ if (matches !== null) {
277+ var headSignsFormat = matches[1] || "";
278+ var wholeNumberFormat = matches[2] || "";
279+ var decimalNumberFormat = matches[4] || "";
280+ var tailingSignsFormat = matches[5] || "";
281+ var commafyNumber = wholeNumberFormat.indexOf(",") > -1;
282+ var builder = MoreUtils_1.NumberStringBuilder.start()
283+ .number(numberValue)
284+ .commafy(commafyNumber)
285+ .integerZeros(1)
286+ .maximumDecimalPlaces(decimalNumberFormat.replace(/ /g, "").length)
287+ .head(headSignsFormat)
288+ .tail(tailingSignsFormat);
289+ return builder.build();
290+ }
291+ /*
292+ * See https://regex101.com/r/Pbx7js/6 for more information.
293+ * 1 = signs, currency, etc.
294+ * 2 = whole number including commas
295+ * 3 = decimal
296+ * 4 = decimal place including spaces
297+ * 5 = signs, currency, etc.
298+ * */
299+ var ZERO_FORMAT_CAPTURE = /^([$%+-]*)([0,]+)?(\.?)([0 ]*)([$%+ -]*)$/gi;
300+ matches = ZERO_FORMAT_CAPTURE.exec(format);
301+ if (matches !== null) {
302+ var headSignsFormat = matches[1] || "";
303+ var wholeNumberFormat = matches[2] || "";
304+ var decimalNumberFormat = matches[4] || "";
305+ var tailingSignsFormat = matches[5] || "";
306+ var commafyNumber = wholeNumberFormat.indexOf(",") > -1;
307+ var builder = MoreUtils_1.NumberStringBuilder.start()
308+ .number(numberValue)
309+ .commafy(commafyNumber)
310+ .integerZeros(wholeNumberFormat.replace(/,/g, "").length)
311+ .decimalZeros(decimalNumberFormat.replace(/ /g, "").length)
312+ .head(headSignsFormat)
313+ .tail(tailingSignsFormat);
314+ return builder.build();
315+ }
316+ // If the format didn't match the patterns above, it is invalid.
317+ throw new Errors_1.ValueError("Invalid format pattern '" + format + "' for TEXT formula.");
318+ }
319+};
320+exports.TEXT = TEXT;
321diff --git a/dist/Utilities/MoreUtils.js b/dist/Utilities/MoreUtils.js
322new file mode 100644
323index 0000000..8516678
324--- /dev/null
325+++ b/dist/Utilities/MoreUtils.js
326@@ -0,0 +1,198 @@
327+"use strict";
328+exports.__esModule = true;
329+/**
330+ * If the value is UNDEFINED, return true.
331+ * @param value - Value to check if undefined.
332+ * @returns {boolean}
333+ */
334+function isUndefined(value) {
335+ return value === undefined;
336+}
337+exports.isUndefined = isUndefined;
338+/**
339+ * If the value is DEFINED, return true.
340+ * @param value - Value to check if is defined.
341+ * @returns {boolean}
342+ */
343+function isDefined(value) {
344+ return value !== undefined;
345+}
346+exports.isDefined = isDefined;
347+/**
348+ * Class for building formatted strings with commas, forced number of leading and trailing zeros, and arbitrary leading
349+ * and trailing strings.
350+ */
351+var NumberStringBuilder = (function () {
352+ function NumberStringBuilder() {
353+ this.shouldUseComma = false;
354+ this.integerZeroCount = 1; // e.g. default to "0.1"
355+ this.decimalZeroCount = 0; // e.g. default to "1"
356+ this.headString = "";
357+ this.tailString = "";
358+ }
359+ /**
360+ * Static builder, easier than `new`.
361+ * @returns {NumberStringBuilder}
362+ */
363+ NumberStringBuilder.start = function () {
364+ return new NumberStringBuilder();
365+ };
366+ /**
367+ * Pads a given string with "0" on the right or left side until it is a certain width.
368+ * @param {string} str - String to pad.
369+ * @param {number} width - Width to pad to. If this is less than the strings length, will do nothing.
370+ * @param {string} type - "right" or "left" side to append zeroes.
371+ * @returns {string}
372+ */
373+ NumberStringBuilder.pad = function (str, width, type) {
374+ var z = '0';
375+ str = str + '';
376+ if (type === "left") {
377+ return str.length >= width ? str : new Array(width - str.length + 1).join(z) + str;
378+ }
379+ else {
380+ return str.length >= width ? str : str + (new Array(width - str.length + 1).join(z));
381+ }
382+ };
383+ /**
384+ * Rounds a number n to a certain number of digits.
385+ * @param n - Number to round.
386+ * @param digits - Digits to round to.
387+ * @returns {number}
388+ */
389+ NumberStringBuilder.round = function (n, digits) {
390+ return Math.round(n * Math.pow(10, digits)) / Math.pow(10, digits);
391+ };
392+ /**
393+ * Set the number that we'll be formatting.
394+ * @param {number} n - Number.
395+ * @returns {NumberStringBuilder}
396+ */
397+ NumberStringBuilder.prototype.number = function (n) {
398+ this.n = n;
399+ return this;
400+ };
401+ /**
402+ * The number of zeros to force on the left side of the decimal.
403+ * @param {number} zeros
404+ * @returns {NumberStringBuilder}
405+ */
406+ NumberStringBuilder.prototype.integerZeros = function (zeros) {
407+ this.integerZeroCount = zeros;
408+ return this;
409+ };
410+ /**
411+ * The number of zeros to force on the right side of the decimal.
412+ * @param {number} zeros
413+ * @returns {NumberStringBuilder}
414+ */
415+ NumberStringBuilder.prototype.decimalZeros = function (zeros) {
416+ this.decimalZeroCount = zeros;
417+ return this;
418+ };
419+ /**
420+ * If you would like to force the maximum number of decimal places, without padding with zeros, set this.
421+ * WARNING: Should not be used in conjunction with decimalZeros().
422+ * @param {number} maxDecimalPlaces
423+ * @returns {NumberStringBuilder}
424+ */
425+ NumberStringBuilder.prototype.maximumDecimalPlaces = function (maxDecimalPlaces) {
426+ this.maxDecimalPlaces = maxDecimalPlaces;
427+ return this;
428+ };
429+ /**
430+ * Should digits to the left side of the decimal use comma-notation?
431+ * @param {boolean} shouldUseComma
432+ * @returns {NumberStringBuilder}
433+ */
434+ NumberStringBuilder.prototype.commafy = function (shouldUseComma) {
435+ this.shouldUseComma = shouldUseComma;
436+ return this;
437+ };
438+ /**
439+ * String to append to the beginning of the final formatted number.
440+ * @param {string} head
441+ * @returns {NumberStringBuilder}
442+ */
443+ NumberStringBuilder.prototype.head = function (head) {
444+ this.headString = head;
445+ return this;
446+ };
447+ /**
448+ * String to append to the end of the final formatted number.
449+ * @param {string} tail
450+ * @returns {NumberStringBuilder}
451+ */
452+ NumberStringBuilder.prototype.tail = function (tail) {
453+ this.tailString = tail;
454+ return this;
455+ };
456+ /**
457+ * Building the string using the rules set in this builder.
458+ * @returns {string}
459+ */
460+ NumberStringBuilder.prototype.build = function () {
461+ var nStr = this.n.toString();
462+ var isInt = this.n % 1 === 0;
463+ var integerPart = isInt ? nStr : nStr.split(".")[0];
464+ integerPart = integerPart.replace("-", "");
465+ var decimalPart = isInt ? "" : nStr.split(".")[1];
466+ // Building integer part
467+ if (this.integerZeroCount > 1) {
468+ integerPart = NumberStringBuilder.pad(integerPart, this.integerZeroCount, "left");
469+ }
470+ // Building decimal part
471+ // If the decimal part is greater than the number of zeros we allow, then we have to round the number.
472+ if (isDefined(this.maxDecimalPlaces)) {
473+ var decimalAsFloat = NumberStringBuilder.round(parseFloat("0." + decimalPart), this.maxDecimalPlaces);
474+ if (decimalAsFloat % 1 === 0) {
475+ integerPart = Math.floor((parseInt(integerPart) + decimalAsFloat)).toString();
476+ integerPart = NumberStringBuilder.pad(integerPart, this.integerZeroCount, "left");
477+ decimalPart = "";
478+ }
479+ else {
480+ decimalPart = decimalAsFloat.toString().split(".")[1];
481+ }
482+ }
483+ else {
484+ if (decimalPart.length > this.decimalZeroCount) {
485+ var decimalAsFloat = NumberStringBuilder.round(parseFloat("0." + decimalPart), this.decimalZeroCount);
486+ var roundedDecimalPart = void 0;
487+ if (decimalAsFloat % 1 === 0) {
488+ integerPart = Math.floor((parseInt(integerPart) + decimalAsFloat)).toString();
489+ integerPart = NumberStringBuilder.pad(integerPart, this.integerZeroCount, "left");
490+ roundedDecimalPart = "";
491+ }
492+ else {
493+ roundedDecimalPart = decimalAsFloat.toString().split(".")[1];
494+ }
495+ decimalPart = NumberStringBuilder.pad(roundedDecimalPart, this.decimalZeroCount, "right");
496+ }
497+ else {
498+ decimalPart = NumberStringBuilder.pad(decimalPart, this.decimalZeroCount, "right");
499+ }
500+ }
501+ // Inserting commas if necessary.
502+ if (this.shouldUseComma) {
503+ integerPart = integerPart.split("").reverse().map(function (digit, index) {
504+ if (index % 3 === 0 && index !== 0) {
505+ return digit + ",";
506+ }
507+ return digit;
508+ }).reverse().join("");
509+ }
510+ if (this.integerZeroCount === 0 && integerPart === "0") {
511+ integerPart = "";
512+ }
513+ if (this.n === 0) {
514+ return this.headString + "." + this.tailString;
515+ }
516+ var trueSign = this.n < 0 ? "-" : "";
517+ if ((this.decimalZeroCount === 0 && isUndefined(this.maxDecimalPlaces)) || isDefined(this.maxDecimalPlaces) && decimalPart === "") {
518+ return trueSign + this.headString + integerPart + this.tailString;
519+ }
520+ return trueSign + this.headString + integerPart + "." + decimalPart + this.tailString;
521+ };
522+ return NumberStringBuilder;
523+}());
524+exports.NumberStringBuilder = NumberStringBuilder;
525diff --git a/dist/Utilities/TypeConverter.js b/dist/Utilities/TypeConverter.js
526index bbb88f3..7cde98a 100644
527--- a/dist/Utilities/TypeConverter.js
528+++ b/dist/Utilities/TypeConverter.js
529@@ -5,6 +5,11 @@ var moment = require("moment");
530 var Errors_1 = require("../Errors");
531 var DateRegExBuilder_1 = require("./DateRegExBuilder");
532 var Cell_1 = require("../Cell");
533+var MONTHDIG_DAYDIG = DateRegExBuilder_1.DateRegExBuilder.DateRegExBuilder()
534+ .start()
535+ .MM().FLEX_DELIMITER().DD_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
536+ .end()
537+ .build();
538 var YEAR_MONTHDIG_DAY = DateRegExBuilder_1.DateRegExBuilder.DateRegExBuilder()
539 .start()
540 .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY().FLEX_DELIMITER_LOOSEDOT().MM().FLEX_DELIMITER_LOOSEDOT().DD_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
541@@ -135,6 +140,18 @@ function matchTimestampAndMutateMoment(timestampString, momentToMutate) {
542 var TypeConverter = (function () {
543 function TypeConverter() {
544 }
545+ /**
546+ * Converts a datetime string to a moment object. Will return undefined if the string can't be converted.
547+ * @param {string} timeString - string to parse and convert.
548+ * @returns {moment.Moment}
549+ */
550+ TypeConverter.stringToMoment = function (timeString) {
551+ var m = TypeConverter.parseStringToMoment(timeString);
552+ if (m === undefined || !m.isValid()) {
553+ return undefined;
554+ }
555+ return m;
556+ };
557 /**
558 * Converts a time-formatted string to a number between 0 and 1, exclusive on 1.
559 * @param timeString
560@@ -189,6 +206,20 @@ var TypeConverter = (function () {
561 }
562 return tmpMoment.add(days, 'days');
563 }
564+ // Check MONTHDIG_DAYDIG, MM(fd)DD, '01/06'
565+ // NOTE: Must come before YEAR_MONTHDIG matching.
566+ if (m === undefined) {
567+ var matches = dateString.match(MONTHDIG_DAYDIG);
568+ if (matches && matches.length >= 10) {
569+ var months = parseInt(matches[1]) - 1; // Months are zero indexed.
570+ var days = parseInt(matches[3]) - 1; // Days are zero indexed.
571+ var tmpMoment = createMoment(moment.utc().get("years"), months, days);
572+ if (matches[8] !== undefined) {
573+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
574+ }
575+ m = tmpMoment;
576+ }
577+ }
578 // Check YEAR_MONTHDIG, YYYY(fd)MM, '1992/06'
579 // NOTE: Must come before YEAR_MONTHDIG_DAY matching.
580 if (m === undefined) {
581@@ -639,11 +670,12 @@ var TypeConverter = (function () {
582 * @returns {number} representing a date
583 */
584 TypeConverter.firstValueAsDateNumber = function (input, coerceBoolean) {
585+ coerceBoolean = coerceBoolean || false;
586 if (input instanceof Array) {
587 if (input.length === 0) {
588 throw new Errors_1.RefError("Reference does not exist.");
589 }
590- return TypeConverter.firstValueAsDateNumber(input[0], coerceBoolean);
591+ return TypeConverter.firstValueAsDateNumber(input[0], coerceBoolean || false);
592 }
593 return TypeConverter.valueToDateNumber(input, coerceBoolean);
594 };
595@@ -724,6 +756,14 @@ var TypeConverter = (function () {
596 TypeConverter.numberToMoment = function (n) {
597 return moment.utc(TypeConverter.ORIGIN_MOMENT).add(n, "days");
598 };
599+ /**
600+ * Converts a number to moment while preserving the decimal part of the number.
601+ * @param n to convert
602+ * @returns {Moment} date
603+ */
604+ TypeConverter.decimalNumberToMoment = function (n) {
605+ return moment.utc(TypeConverter.ORIGIN_MOMENT).add(n * TypeConverter.SECONDS_IN_DAY * 1000, "milliseconds");
606+ };
607 /**
608 * Using timestamp units, create a time number between 0 and 1, exclusive on end.
609 * @param hours
610diff --git a/package.json b/package.json
611index 833330c..ce45625 100644
612--- a/package.json
613+++ b/package.json
614@@ -6,8 +6,8 @@
615 "clean": "rm -rf dist/* && rm -rf test_output/*",
616 "build": "tsc",
617 "docs": "./docs.sh src/Formulas",
618- "test": "./tests.sh",
619- "test:quiet": "./tests.sh | grep -v Test:"
620+ "test": "rm -rf test_output/* && ./tests.sh",
621+ "test:quiet": "rm -rf test_output/* && ./tests.sh | grep -v Test:"
622 },
623 "author": "vogtb <bvogt at gmail.com>",
624 "license": "MIT",
625diff --git a/src/Formulas/AllFormulas.ts b/src/Formulas/AllFormulas.ts
626index 45323ab..aa374de 100644
627--- a/src/Formulas/AllFormulas.ts
628+++ b/src/Formulas/AllFormulas.ts
629@@ -227,7 +227,8 @@ import {
630 LOWER,
631 UPPER,
632 T,
633- ROMAN
634+ ROMAN,
635+ TEXT
636 } from "./Text"
637 import {
638 DATE,
639@@ -515,5 +516,6 @@ export {
640 COLUMNS,
641 ROWS,
642 SERIESSUM,
643- ROMAN
644+ ROMAN,
645+ TEXT
646 }
647\ No newline at end of file
648diff --git a/src/Formulas/Text.ts b/src/Formulas/Text.ts
649index d960e89..04c9334 100644
650--- a/src/Formulas/Text.ts
651+++ b/src/Formulas/Text.ts
652@@ -10,6 +10,15 @@ import {
653 RefError,
654 NAError
655 } from "../Errors";
656+import {
657+ Filter
658+} from "../Utilities/Filter";
659+import {
660+ isDefined,
661+ NumberStringBuilder
662+} from "../Utilities/MoreUtils";
663+import {ROUND} from "./Math";
664+import {min} from "moment";
665
666 /**
667 * Computes the value of a Roman numeral.
668@@ -412,7 +421,6 @@ let CONVERT = function (value, startUnit, endUnit) {
669
670 // Return error if a unit does not exist
671 if (from === null || to === null) {
672- console.log(from, to);
673 throw new NAError("Invalid units for conversion.");
674 }
675
676@@ -481,7 +489,7 @@ let T = function (value) {
677 * Converts a number into a Roman numeral.
678 * @param value - The value to convert. Must be between 0 and 3999.
679 * @constructor
680- * TODO: Second parameter should be 'rule_relaxation'
681+ * TODO: Second parameter should be 'rule_relaxation'.
682 */
683 let ROMAN = function (value) {
684 ArgsChecker.checkLength(arguments, 1, "ROMAN");
685@@ -492,6 +500,7 @@ let ROMAN = function (value) {
686 }
687 // The MIT License
688 // Copyright (c) 2008 Steven Levithan
689+ // https://stackoverflow.com/questions/9083037/convert-a-number-into-a-roman-numeral-in-javascript
690 let digits = String(value).split('');
691 let key = ['',
692 'C',
693@@ -532,6 +541,204 @@ let ROMAN = function (value) {
694 return new Array(+digits.join('') + 1).join('M') + roman;
695 };
696
697+/**
698+ * Converts a number into text according to a given format.
699+ * @param value - The value to be converted.
700+ * @param format - Text which defines the format. "0" forces the display of zeros, while "#" suppresses the display of
701+ * zeros. For example TEXT(22.1,"000.00") produces 022.10, while TEXT(22.1,"###.##") produces 22.1, and
702+ * TEXT(22.405,"00.00") results in 22.41. To format days: "dddd" indicates full name of the day of the week, "ddd"
703+ * short name of the day of the week, "dd" indicates the day of the month as two digits, "d" indicates day of the month
704+ * as one or two digits, "mmmmm" indicates the first letter in the month of the year, "mmmm" indicates the full name of
705+ * the month of the year, "mmm" indicates short name of the month of the year, "mm" indicates month of the year as two
706+ * digits or the number of minutes in a time, depending on whether it follows yy or dd, or if it follows hh, "m" month
707+ * of the year as one or two digits or the number of minutes in a time, depending on whether it follows yy or dd, or if
708+ * it follows hh, "yyyy" indicates year as four digits, "yy" and "y" indicate year as two digits, "hh" indicates hour
709+ * on a 24-hour clock, "h" indicates hour on a 12-hour clock, "ss.000" indicates milliseconds in a time, "ss" indicates
710+ * seconds in a time, "AM/PM" or "A/P" indicate displaying hours based on a 12-hour clock and showing AM or PM
711+ * depending on the time of day. Eg: `TEXT("01/09/2012 10:04:33AM", "mmmm-dd-yyyy, hh:mm AM/PM")` would result in
712+ * "January-09-2012, 10:04 AM".
713+ * @constructor
714+ */
715+let TEXT = function (value, format) {
716+ ArgsChecker.checkLength(arguments, 2, "TEXT");
717+ value = TypeConverter.firstValue(value);
718+
719+
720+ function splitReplace(values: Array<any>, regex, index) : Array<any> {
721+ return values.map(function (value) {
722+ if (typeof value === "number") {
723+ return [value];
724+ } else if (value instanceof Array) {
725+ return splitReplace(value, regex, index);
726+ } else {
727+ let splits = value.split(regex);
728+ let building = [];
729+ if (splits.length === 1) {
730+ return [splits];
731+ }
732+ splits.map(function (splitValue, splitIndex) {
733+ building.push(splitValue);
734+ if (splitIndex !== splits.length-1) {
735+ building.push(index);
736+ }
737+ });
738+ return building;
739+ }
740+ });
741+ }
742+
743+ // Short cut for booleans
744+ if (typeof value === "boolean") {
745+ return TypeConverter.valueToString(value);
746+ }
747+
748+ // If the format matches the date format
749+ if (format.match(/^.*(d|D|M|m|yy|Y|HH|hh|h|s|S|AM|PM|am|pm|A\/P|\*).*$/g)) {
750+ // If the format contains both, throw error
751+ if (format.indexOf("#") > -1 || format.indexOf("0") > -1) {
752+ throw new ValueError("Invalid format pattern '" + format + "' for TEXT formula.");
753+ }
754+ let valueAsMoment;
755+ if (typeof value === "string") {
756+ valueAsMoment = TypeConverter.stringToMoment(value);
757+ if (valueAsMoment === undefined) {
758+ valueAsMoment = TypeConverter.decimalNumberToMoment(TypeConverter.valueToNumber(value));
759+ }
760+ } else {
761+ valueAsMoment = TypeConverter.decimalNumberToMoment(TypeConverter.valueToNumber(value));
762+ }
763+ let replacementPairs = [
764+ // full name of the day of the week
765+ [/dddd/gi, valueAsMoment.format("dddd")],
766+ // short name of the day of the week
767+ [/ddd/gi, valueAsMoment.format("ddd")],
768+ // day of the month as two digits
769+ [/dd/gi, valueAsMoment.format("DD")],
770+ // day of the month as one or two digits
771+ [/d/gi, valueAsMoment.format("d")],
772+ // first letter in the month of the year
773+ [/mmmmm/gi, valueAsMoment.format("MMMM").charAt(0)],
774+ // full name of the month of the year
775+ [/mmmm/gi, valueAsMoment.format("MMMM")],
776+ // short name of the month of the year
777+ [/mmm/gi, valueAsMoment.format("MMM")],
778+ // month of the year as two digits or the number of minutes in a time
779+ [/mm/gi, function (monthOrMinute : string) {
780+ return monthOrMinute === "month" ? valueAsMoment.format("MM") : valueAsMoment.format("mm");
781+ }],
782+ // month of the year as one or two digits or the number of minutes in a time
783+ [/m/g, function (monthOrMinute : string) {
784+ return monthOrMinute === "month" ? valueAsMoment.format("M") : valueAsMoment.format("m");
785+ }],
786+ // year as four digits
787+ [/yyyy/gi, valueAsMoment.format("YYYY")],
788+ // year as two digits
789+ [/yy/gi, valueAsMoment.format("YY")],
790+ // year as two digits
791+ [/y/gi, valueAsMoment.format("YY")],
792+ // hour on a 24-hour clock
793+ [/HH/g, valueAsMoment.format("HH")],
794+ // hour on a 12-hour clock
795+ [/hh/g, valueAsMoment.format("hh")],
796+ // hour on a 12-hour clock
797+ [/h/gi, valueAsMoment.format("hh")],
798+ // milliseconds in a time
799+ [/ss\.000/gi, valueAsMoment.format("ss.SSS")],
800+ // seconds in a time
801+ [/ss/gi, valueAsMoment.format("ss")],
802+ // seconds in a time
803+ [/s/gi, valueAsMoment.format("ss")],
804+ [/AM\/PM/gi, valueAsMoment.format("A")],
805+ // displaying hours based on a 12-hour clock and showing AM or PM depending on the time of day
806+ [/A\/P/gi, valueAsMoment.format("A").charAt(0)]
807+ ];
808+
809+ let builtList = [format];
810+ replacementPairs.map(function (pair, pairIndex) {
811+ let regex = pair[0];
812+ builtList = splitReplace(builtList, regex, pairIndex);
813+ });
814+ let lastRegEx = "";
815+ return Filter.flatten(builtList).map(function (val) {
816+ if (typeof val === "number") {
817+ if (typeof replacementPairs[val][1] === "function") {
818+ let monthOrMinute = "month";
819+ // Hack-ish way of determining if MM, mm, M, or m should be evaluated as minute or month.
820+ let lastRegExWasHour = lastRegEx.toString() === new RegExp("hh", "g").toString()
821+ || lastRegEx.toString() === new RegExp("HH", "g").toString()
822+ || lastRegEx.toString() === new RegExp("h", "g").toString();
823+ if (lastRegExWasHour) {
824+ monthOrMinute = "minute";
825+ }
826+ lastRegEx = replacementPairs[val][0];
827+ return replacementPairs[val][1](monthOrMinute);
828+ }
829+ lastRegEx = replacementPairs[val][0];
830+ return replacementPairs[val][1];
831+ }
832+ return val;
833+ }).join("");
834+
835+
836+ } else {
837+ let numberValue = TypeConverter.valueToNumber(value);
838+
839+ // Format string can't contain both 0 and #.
840+ if (format.indexOf("#") > -1 && format.indexOf("0") > -1) {
841+ throw new ValueError("Invalid format pattern '" + format + "' for TEXT formula.");
842+ }
843+
844+ // See https://regex101.com/r/Jji2Ng/8 for more information.
845+ const POUND_SIGN_FORMAT_CAPTURE = /^([$%+-]*)([#,]+)?(\.?)([# ]*)([$%+ -]*)$/gi;
846+
847+ let matches = POUND_SIGN_FORMAT_CAPTURE.exec(format);
848+ if (matches !== null) {
849+ let headSignsFormat = matches[1] || "";
850+ let wholeNumberFormat = matches[2] || "";
851+ let decimalNumberFormat = matches[4] || "";
852+ let tailingSignsFormat = matches[5] || "";
853+ let commafyNumber = wholeNumberFormat.indexOf(",") > -1;
854+ let builder = NumberStringBuilder.start()
855+ .number(numberValue)
856+ .commafy(commafyNumber)
857+ .integerZeros(1)
858+ .maximumDecimalPlaces(decimalNumberFormat.replace(/ /g, "").length)
859+ .head(headSignsFormat)
860+ .tail(tailingSignsFormat);
861+ return builder.build();
862+ }
863+
864+ /*
865+ * See https://regex101.com/r/Pbx7js/6 for more information.
866+ * 1 = signs, currency, etc.
867+ * 2 = whole number including commas
868+ * 3 = decimal
869+ * 4 = decimal place including spaces
870+ * 5 = signs, currency, etc.
871+ * */
872+ const ZERO_FORMAT_CAPTURE = /^([$%+-]*)([0,]+)?(\.?)([0 ]*)([$%+ -]*)$/gi;
873+ matches = ZERO_FORMAT_CAPTURE.exec(format);
874+ if (matches !== null) {
875+ let headSignsFormat = matches[1] || "";
876+ let wholeNumberFormat = matches[2] || "";
877+ let decimalNumberFormat = matches[4] || "";
878+ let tailingSignsFormat = matches[5] || "";
879+ let commafyNumber = wholeNumberFormat.indexOf(",") > -1;
880+ let builder = NumberStringBuilder.start()
881+ .number(numberValue)
882+ .commafy(commafyNumber)
883+ .integerZeros(wholeNumberFormat.replace(/,/g, "").length)
884+ .decimalZeros(decimalNumberFormat.replace(/ /g, "").length)
885+ .head(headSignsFormat)
886+ .tail(tailingSignsFormat);
887+ return builder.build();
888+ }
889+
890+ // If the format didn't match the patterns above, it is invalid.
891+ throw new ValueError("Invalid format pattern '" + format + "' for TEXT formula.");
892+ }
893+};
894+
895 export {
896 ARABIC,
897 CHAR,
898@@ -543,5 +750,6 @@ export {
899 LOWER,
900 UPPER,
901 T,
902- ROMAN
903+ ROMAN,
904+ TEXT
905 }
906\ No newline at end of file
907diff --git a/src/Utilities/MoreUtils.ts b/src/Utilities/MoreUtils.ts
908new file mode 100644
909index 0000000..cb0e2b4
910--- /dev/null
911+++ b/src/Utilities/MoreUtils.ts
912@@ -0,0 +1,215 @@
913+/**
914+ * If the value is UNDEFINED, return true.
915+ * @param value - Value to check if undefined.
916+ * @returns {boolean}
917+ */
918+function isUndefined(value : any) : boolean {
919+ return value === undefined;
920+}
921+
922+/**
923+ * If the value is DEFINED, return true.
924+ * @param value - Value to check if is defined.
925+ * @returns {boolean}
926+ */
927+function isDefined(value : any) : boolean {
928+ return value !== undefined;
929+}
930+
931+
932+/**
933+ * Class for building formatted strings with commas, forced number of leading and trailing zeros, and arbitrary leading
934+ * and trailing strings.
935+ */
936+class NumberStringBuilder {
937+ private n : number;
938+ private shouldUseComma : boolean = false;
939+ private integerZeroCount : number = 1; // e.g. default to "0.1"
940+ private decimalZeroCount : number = 0; // e.g. default to "1"
941+ private maxDecimalPlaces : number;
942+ private headString : string = "";
943+ private tailString : string = "";
944+
945+ /**
946+ * Static builder, easier than `new`.
947+ * @returns {NumberStringBuilder}
948+ */
949+ static start() : NumberStringBuilder {
950+ return new NumberStringBuilder();
951+ }
952+
953+ /**
954+ * Pads a given string with "0" on the right or left side until it is a certain width.
955+ * @param {string} str - String to pad.
956+ * @param {number} width - Width to pad to. If this is less than the strings length, will do nothing.
957+ * @param {string} type - "right" or "left" side to append zeroes.
958+ * @returns {string}
959+ */
960+ private static pad(str : string, width : number, type : string) : string {
961+ let z = '0';
962+ str = str + '';
963+ if (type === "left") {
964+ return str.length >= width ? str : new Array(width - str.length + 1).join(z) + str;
965+ } else {
966+ return str.length >= width ? str : str + (new Array(width - str.length + 1).join(z));
967+ }
968+ }
969+
970+ /**
971+ * Rounds a number n to a certain number of digits.
972+ * @param n - Number to round.
973+ * @param digits - Digits to round to.
974+ * @returns {number}
975+ */
976+ private static round(n, digits) {
977+ return Math.round(n * Math.pow(10, digits)) / Math.pow(10, digits);
978+ }
979+
980+ /**
981+ * Set the number that we'll be formatting.
982+ * @param {number} n - Number.
983+ * @returns {NumberStringBuilder}
984+ */
985+ public number(n : number) : NumberStringBuilder {
986+ this.n = n;
987+ return this;
988+ }
989+
990+ /**
991+ * The number of zeros to force on the left side of the decimal.
992+ * @param {number} zeros
993+ * @returns {NumberStringBuilder}
994+ */
995+ public integerZeros(zeros : number) : NumberStringBuilder {
996+ this.integerZeroCount = zeros;
997+ return this;
998+ }
999+
1000+ /**
1001+ * The number of zeros to force on the right side of the decimal.
1002+ * @param {number} zeros
1003+ * @returns {NumberStringBuilder}
1004+ */
1005+ public decimalZeros(zeros : number) : NumberStringBuilder {
1006+ this.decimalZeroCount = zeros;
1007+ return this;
1008+ }
1009+
1010+ /**
1011+ * If you would like to force the maximum number of decimal places, without padding with zeros, set this.
1012+ * WARNING: Should not be used in conjunction with decimalZeros().
1013+ * @param {number} maxDecimalPlaces
1014+ * @returns {NumberStringBuilder}
1015+ */
1016+ public maximumDecimalPlaces(maxDecimalPlaces: number) : NumberStringBuilder {
1017+ this.maxDecimalPlaces = maxDecimalPlaces;
1018+ return this;
1019+ }
1020+
1021+ /**
1022+ * Should digits to the left side of the decimal use comma-notation?
1023+ * @param {boolean} shouldUseComma
1024+ * @returns {NumberStringBuilder}
1025+ */
1026+ public commafy(shouldUseComma : boolean) : NumberStringBuilder {
1027+ this.shouldUseComma = shouldUseComma;
1028+ return this;
1029+ }
1030+
1031+ /**
1032+ * String to append to the beginning of the final formatted number.
1033+ * @param {string} head
1034+ * @returns {NumberStringBuilder}
1035+ */
1036+ public head(head : string) : NumberStringBuilder {
1037+ this.headString = head;
1038+ return this;
1039+ }
1040+
1041+ /**
1042+ * String to append to the end of the final formatted number.
1043+ * @param {string} tail
1044+ * @returns {NumberStringBuilder}
1045+ */
1046+ public tail(tail : string) : NumberStringBuilder {
1047+ this.tailString = tail;
1048+ return this;
1049+ }
1050+
1051+ /**
1052+ * Building the string using the rules set in this builder.
1053+ * @returns {string}
1054+ */
1055+ public build() : string {
1056+ let nStr = this.n.toString();
1057+ let isInt = this.n % 1 === 0;
1058+ let integerPart = isInt ? nStr : nStr.split(".")[0];
1059+ integerPart = integerPart.replace("-", "");
1060+ let decimalPart = isInt ? "" : nStr.split(".")[1];
1061+
1062+ // Building integer part
1063+ if (this.integerZeroCount > 1) {
1064+ integerPart = NumberStringBuilder.pad(integerPart, this.integerZeroCount, "left");
1065+ }
1066+
1067+ // Building decimal part
1068+ // If the decimal part is greater than the number of zeros we allow, then we have to round the number.
1069+ if (isDefined(this.maxDecimalPlaces)) {
1070+ let decimalAsFloat = NumberStringBuilder.round(parseFloat("0."+decimalPart), this.maxDecimalPlaces);
1071+ if (decimalAsFloat % 1 === 0) {
1072+ integerPart = Math.floor((parseInt(integerPart) + decimalAsFloat)).toString();
1073+ integerPart = NumberStringBuilder.pad(integerPart, this.integerZeroCount, "left");
1074+ decimalPart = "";
1075+ } else {
1076+ decimalPart = decimalAsFloat.toString().split(".")[1];
1077+ }
1078+ } else {
1079+ if (decimalPart.length > this.decimalZeroCount) {
1080+ let decimalAsFloat = NumberStringBuilder.round(parseFloat("0."+decimalPart), this.decimalZeroCount);
1081+ let roundedDecimalPart;
1082+
1083+ if (decimalAsFloat % 1 === 0) {
1084+ integerPart = Math.floor((parseInt(integerPart) + decimalAsFloat)).toString();
1085+ integerPart = NumberStringBuilder.pad(integerPart, this.integerZeroCount, "left");
1086+ roundedDecimalPart = "";
1087+ } else {
1088+ roundedDecimalPart = decimalAsFloat.toString().split(".")[1];
1089+ }
1090+ decimalPart = NumberStringBuilder.pad(roundedDecimalPart, this.decimalZeroCount, "right");
1091+ } else {
1092+ decimalPart = NumberStringBuilder.pad(decimalPart, this.decimalZeroCount, "right");
1093+ }
1094+ }
1095+
1096+
1097+ // Inserting commas if necessary.
1098+ if (this.shouldUseComma) {
1099+ integerPart = integerPart.split("").reverse().map(function (digit, index) {
1100+ if (index % 3 === 0 && index !== 0) {
1101+ return digit + ",";
1102+ }
1103+ return digit;
1104+ }).reverse().join("");
1105+ }
1106+
1107+ if (this.integerZeroCount === 0 && integerPart === "0") {
1108+ integerPart = "";
1109+ }
1110+
1111+ if (this.n === 0) {
1112+ return this.headString + "." + this.tailString;
1113+ }
1114+ let trueSign = this.n < 0 ? "-" : "";
1115+
1116+ if ((this.decimalZeroCount === 0 && isUndefined(this.maxDecimalPlaces)) || isDefined(this.maxDecimalPlaces) && decimalPart === "") {
1117+ return trueSign + this.headString + integerPart + this.tailString;
1118+ }
1119+ return trueSign + this.headString + integerPart + "." + decimalPart + this.tailString;
1120+ }
1121+}
1122+
1123+export {
1124+ isDefined,
1125+ isUndefined,
1126+ NumberStringBuilder
1127+}
1128\ No newline at end of file
1129diff --git a/src/Utilities/TypeConverter.ts b/src/Utilities/TypeConverter.ts
1130index f0effb7..761d254 100644
1131--- a/src/Utilities/TypeConverter.ts
1132+++ b/src/Utilities/TypeConverter.ts
1133@@ -12,6 +12,11 @@ import {
1134 Cell
1135 } from "../Cell";
1136
1137+const MONTHDIG_DAYDIG = DateRegExBuilder.DateRegExBuilder()
1138+ .start()
1139+ .MM().FLEX_DELIMITER().DD_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
1140+ .end()
1141+ .build();
1142 const YEAR_MONTHDIG_DAY = DateRegExBuilder.DateRegExBuilder()
1143 .start()
1144 .OPTIONAL_DAYNAME().OPTIONAL_COMMA().YYYY().FLEX_DELIMITER_LOOSEDOT().MM().FLEX_DELIMITER_LOOSEDOT().DD_W_SPACE().OPTIONAL_TIMESTAMP_CAPTURE_GROUP()
1145@@ -141,6 +146,19 @@ class TypeConverter {
1146 private static SECONDS_IN_DAY = 86400;
1147
1148
1149+ /**
1150+ * Converts a datetime string to a moment object. Will return undefined if the string can't be converted.
1151+ * @param {string} timeString - string to parse and convert.
1152+ * @returns {moment.Moment}
1153+ */
1154+ static stringToMoment(timeString : string) : moment.Moment {
1155+ let m = TypeConverter.parseStringToMoment(timeString);
1156+ if (m === undefined || !m.isValid()) {
1157+ return undefined;
1158+ }
1159+ return m;
1160+ }
1161+
1162 /**
1163 * Converts a time-formatted string to a number between 0 and 1, exclusive on 1.
1164 * @param timeString
1165@@ -195,6 +213,21 @@ class TypeConverter {
1166 return tmpMoment.add(days, 'days');
1167 }
1168
1169+ // Check MONTHDIG_DAYDIG, MM(fd)DD, '01/06'
1170+ // NOTE: Must come before YEAR_MONTHDIG matching.
1171+ if (m === undefined) {
1172+ let matches = dateString.match(MONTHDIG_DAYDIG);
1173+ if (matches && matches.length >= 10) {
1174+ let months = parseInt(matches[1]) - 1; // Months are zero indexed.
1175+ let days = parseInt(matches[3]) - 1; // Days are zero indexed.
1176+ let tmpMoment = createMoment(moment.utc().get("years"), months, days);
1177+ if (matches[8] !== undefined) {
1178+ tmpMoment = matchTimestampAndMutateMoment(matches[8], tmpMoment);
1179+ }
1180+ m = tmpMoment
1181+ }
1182+ }
1183+
1184 // Check YEAR_MONTHDIG, YYYY(fd)MM, '1992/06'
1185 // NOTE: Must come before YEAR_MONTHDIG_DAY matching.
1186 if (m === undefined) {
1187@@ -645,11 +678,12 @@ class TypeConverter {
1188 * @returns {number} representing a date
1189 */
1190 public static firstValueAsDateNumber(input: any, coerceBoolean?: boolean) : number {
1191+ coerceBoolean = coerceBoolean || false;
1192 if (input instanceof Array) {
1193 if (input.length === 0) {
1194 throw new RefError("Reference does not exist.");
1195 }
1196- return TypeConverter.firstValueAsDateNumber(input[0], coerceBoolean);
1197+ return TypeConverter.firstValueAsDateNumber(input[0], coerceBoolean || false);
1198 }
1199 return TypeConverter.valueToDateNumber(input, coerceBoolean);
1200 }
1201@@ -689,7 +723,7 @@ class TypeConverter {
1202 return value;
1203 } else if (typeof value === "string") {
1204 try {
1205- return TypeConverter.stringToDateNumber(value)
1206+ return TypeConverter.stringToDateNumber(value);
1207 } catch (e) {
1208 if (TypeConverter.canCoerceToNumber(value)) {
1209 return TypeConverter.valueToNumber(value);
1210@@ -731,6 +765,15 @@ class TypeConverter {
1211 return moment.utc(TypeConverter.ORIGIN_MOMENT).add(n, "days");
1212 }
1213
1214+ /**
1215+ * Converts a number to moment while preserving the decimal part of the number.
1216+ * @param n to convert
1217+ * @returns {Moment} date
1218+ */
1219+ public static decimalNumberToMoment(n : number) : moment.Moment {
1220+ return moment.utc(TypeConverter.ORIGIN_MOMENT).add(n * TypeConverter.SECONDS_IN_DAY * 1000, "milliseconds");
1221+ }
1222+
1223 /**
1224 * Using timestamp units, create a time number between 0 and 1, exclusive on end.
1225 * @param hours
1226diff --git a/tests/Formulas/DateFormulasTestTimeOverride.ts b/tests/Formulas/DateFormulasTestTimeOverride.ts
1227index 0b27752..ce4179a 100644
1228--- a/tests/Formulas/DateFormulasTestTimeOverride.ts
1229+++ b/tests/Formulas/DateFormulasTestTimeOverride.ts
1230@@ -7,21 +7,10 @@ import * as ERRORS from "../../src/Errors";
1231 import {
1232 assertEquals,
1233 catchAndAssertEquals,
1234- test
1235+ test,
1236+ lockDate
1237 } from "../Utils/Asserts"
1238
1239-
1240-// WARNING: Locking in Date by overriding prototypes.
1241-function lockDate(year, month, day, hour, minute, second) {
1242- var d = new Date(year, month, day, hour, minute, second);
1243- Date.prototype.constructor = function () {
1244- return d;
1245- };
1246- Date.now = function () {
1247- return +(d);
1248- };
1249-}
1250-
1251 test("NOW", function(){
1252 lockDate(2012, 11, 10, 4, 55, 4);
1253 assertEquals(NOW(), 41253.45490740741);
1254diff --git a/tests/Formulas/TextTest.ts b/tests/Formulas/TextTest.ts
1255index 467a39a..8bb2e19 100644
1256--- a/tests/Formulas/TextTest.ts
1257+++ b/tests/Formulas/TextTest.ts
1258@@ -9,14 +9,16 @@ import {
1259 LOWER,
1260 UPPER,
1261 T,
1262- ROMAN
1263+ ROMAN,
1264+ TEXT
1265 } from "../../src/Formulas/Text";
1266 import * as ERRORS from "../../src/Errors";
1267 import {
1268 assertEquals,
1269 assertArrayEquals,
1270 catchAndAssertEquals,
1271- test
1272+ test,
1273+ lockDate
1274 } from "../Utils/Asserts";
1275
1276
1277@@ -198,4 +200,152 @@ test("ROMAN", function(){
1278 catchAndAssertEquals(function() {
1279 ROMAN(0);
1280 }, ERRORS.VALUE_ERROR);
1281+ catchAndAssertEquals(function() {
1282+ ROMAN.apply(this, [])
1283+ }, ERRORS.NA_ERROR);
1284+});
1285+
1286+test("TEXT", function(){
1287+ lockDate(2012, 1, 9, 10, 22, 33);
1288+ assertEquals(TEXT("01/09/2012 10:22:33AM", "dd mm yyyy"), "09 01 2012");
1289+ assertEquals(TEXT("01/09/2012 10:22:33AM", "dddd dd mm yyyy"), "Monday 09 01 2012");
1290+ assertEquals(TEXT("01/09/2012 10:22:33AM", "dddd"), "Monday");
1291+ assertEquals(TEXT("01/10/2012 10:22:33AM", "dddd"), "Tuesday");
1292+ assertEquals(TEXT("01/11/2012 10:22:33AM", "dddd"), "Wednesday");
1293+ assertEquals(TEXT("01/12/2012 10:22:33AM", "dddd"), "Thursday");
1294+ assertEquals(TEXT("01/13/2012 10:22:33AM", "dddd"), "Friday");
1295+ assertEquals(TEXT("01/14/2012 10:22:33AM", "dddd"), "Saturday");
1296+ assertEquals(TEXT("01/15/2012 10:22:33AM", "dddd"), "Sunday");
1297+ assertEquals(TEXT("01/09/2012 10:22:33AM", "dDDd"), "Monday");
1298+ assertEquals(TEXT("01/09/2012 10:22:33AM", "ddd"), "Mon");
1299+ assertEquals(TEXT("01/10/2012 10:22:33AM", "ddd"), "Tue");
1300+ assertEquals(TEXT("01/11/2012 10:22:33AM", "ddd"), "Wed");
1301+ assertEquals(TEXT("01/12/2012 10:22:33AM", "ddd"), "Thu");
1302+ assertEquals(TEXT("01/13/2012 10:22:33AM", "ddd"), "Fri");
1303+ assertEquals(TEXT("01/14/2012 10:22:33AM", "ddd"), "Sat");
1304+ assertEquals(TEXT("01/15/2012 10:22:33AM", "ddd"), "Sun");
1305+ assertEquals(TEXT("01/15/2012 10:22:33AM", "DDD"), "Sun");
1306+ assertEquals(TEXT("01/09/2012 10:22:33AM", "dd"), "09");
1307+ assertEquals(TEXT("01/09/2012 10:22:33AM", "DD"), "09");
1308+ assertEquals(TEXT("01/09/2012 10:22:33AM", "d"), "1");
1309+ assertEquals(TEXT("01/09/2012 10:22:33AM", "D"), "1");
1310+ assertEquals(TEXT("01/09/2012 10:22:33AM", "mmmmm"), "J");
1311+ assertEquals(TEXT("02/09/2012 10:22:33AM", "mmmmm"), "F");
1312+ assertEquals(TEXT("02/09/2012 10:22:33AM", "MMMMM"), "F");
1313+ assertEquals(TEXT("01/09/2012 10:22:33AM", "mmmm"), "January");
1314+ assertEquals(TEXT("02/09/2012 10:22:33AM", "mmmm"), "February");
1315+ assertEquals(TEXT("02/09/2012 10:22:33AM", "MMMM"), "February");
1316+ assertEquals(TEXT("01/09/2012 10:22:33AM", "mmm"), "Jan");
1317+ assertEquals(TEXT("02/09/2012 10:22:33AM", "mmm"), "Feb");
1318+ assertEquals(TEXT("02/09/2012 10:22:33AM", "MMM"), "Feb");
1319+ assertEquals(TEXT("01/09/2012 10:22:33AM", "mm"), "01");
1320+ assertEquals(TEXT("02/09/2012 10:22:33AM", "mm"), "02");
1321+ assertEquals(TEXT("02/09/2012 10:22:33AM", "MM"), "02");
1322+ assertEquals(TEXT("01/09/2012 10:04:33AM", "HH mm"), "10 04");
1323+ assertEquals(TEXT("01/09/2012 10:04:33AM", "HH m"), "10 4");
1324+ assertEquals(TEXT("01/09/2012 10:04:33AM", "hh m"), "10 4");
1325+ assertEquals(TEXT("01/09/2012 10:04:33AM", "m"), "1");
1326+ assertEquals(TEXT("01/09/2012 10:04:33AM", "yyyy"), "2012");
1327+ assertEquals(TEXT("01/09/2012 10:04:33AM", "YYYY"), "2012");
1328+ assertEquals(TEXT("01/09/2012 10:04:33AM", "yy"), "12");
1329+ assertEquals(TEXT("01/09/2012 10:04:33AM", "YY"), "12");
1330+ assertEquals(TEXT("01/09/1912 10:04:33AM", "yy"), "12");
1331+ assertEquals(TEXT("01/09/2012 10:04:33PM", "HH"), "22");
1332+ assertEquals(TEXT("01/09/2012 10:04:33PM", "hh"), "10");
1333+ // TODO: This will be fixed as soon as we allow sub-second date-string parsing in TypeConverter.
1334+ // assertEquals(TEXT("01/09/2012 10:04:33.123", "ss.000"), "33.123");
1335+ assertEquals(TEXT("01/09/2012 10:04:33AM", "ss"), "33");
1336+ assertEquals(TEXT("01/09/2012 10:04:33AM", "AM/PM"), "AM");
1337+ assertEquals(TEXT("01/09/2012 10:04:33PM", "AM/PM"), "PM");
1338+ assertEquals(TEXT("01/09/2012 10:04:33AM", "A/P"), "A");
1339+ assertEquals(TEXT("01/09/2012 10:04:33PM", "A/P"), "P");
1340+ assertEquals(TEXT("01/09/2012 10:04:33AM", "mmmm-dd-yyyy, hh:mm A/P"), "January-09-2012, 10:04 A");
1341+ assertEquals(TEXT("01/09/2012 10:04:33PM", "mmmm-dd-yyyy, HH:mm A/P"), "January-09-2012, 22:04 P");
1342+ assertEquals(TEXT(44564.111, "dd mm yyyy HH:mm:ss"), "03 01 2022 02:39:50");
1343+ assertEquals(TEXT(44564.111, "dd mmmm yyyy HH:mm:ss"), "03 January 2022 02:39:50");
1344+ assertEquals(TEXT(44564, "dd mmmm yyyy HH:mm:ss"), "03 January 2022 00:00:00");
1345+ assertEquals(TEXT(64, "dd mmmm yyyy HH:mm:ss"), "04 March 1900 00:00:00");
1346+ assertEquals(TEXT(false, "dd mmmm yyyy HH:mm:ss"), "FALSE");
1347+ assertEquals(TEXT(true, "dd mmmm yyyy HH:mm:ss"), "TRUE");
1348+ assertEquals(TEXT(-164, "dd mmmm yyyy HH:mm:ss"), "19 July 1899 00:00:00");
1349+ // "##.##" formatting
1350+ assertEquals(TEXT(12.3, "###.##"), "12.3");
1351+ assertEquals(TEXT(12.3333, "###.##"), "12.33");
1352+ assertEquals(TEXT(0.001, "#.###############"), "0.001");
1353+ assertEquals(TEXT(0, "#.#"), ".");
1354+ assertEquals(TEXT(0.99, "#"), "1");
1355+ assertEquals(TEXT(0, "$#.#"), "$.");
1356+ assertEquals(TEXT(1231213131.32232, "######,###.#"), "1,231,213,131.3");
1357+ assertEquals(TEXT(123333333333, "######,###.#"), "123,333,333,333");
1358+ assertEquals(TEXT(1224324333.36543, ",###.#"), "1,224,324,333.4");
1359+ assertEquals(TEXT(-12.3, "###.##"), "-12.3");
1360+ assertEquals(TEXT(-12.3333, "###.##"), "-12.33");
1361+ assertEquals(TEXT(-0.001, "#.###############"), "-0.001");
1362+ assertEquals(TEXT(-1231213131.32232, "######,###.#"), "-1,231,213,131.3");
1363+ assertEquals(TEXT(-123333333333, "######,###.#"), "-123,333,333,333");
1364+ assertEquals(TEXT(-1224324333.36543, ",###.#"), "-1,224,324,333.4");
1365+ assertEquals(TEXT(12.3, "$###.##"), "$12.3");
1366+ assertEquals(TEXT(12.3, "%###.##"), "%12.3");
1367+ assertEquals(TEXT(12.3, "$+-+%###.##$+-+"), "$+-+%12.3$+-+");
1368+ // "00.00" formatting
1369+ assertEquals(TEXT(12.3, "00"), "12");
1370+ assertEquals(TEXT(12.9, "00"), "13");
1371+ assertEquals(TEXT(12.3, "00.0"), "12.3");
1372+ assertEquals(TEXT(12.3, "000.0"), "012.3");
1373+ assertEquals(TEXT(12.3, "000.00"), "012.30");
1374+ assertEquals(TEXT(-12.3, "00"), "-12");
1375+ assertEquals(TEXT(-12.9, "00"), "-13");
1376+ assertEquals(TEXT(-12.3, "00.0"), "-12.3");
1377+ assertEquals(TEXT(-12.3, "000.0"), "-012.3");
1378+ assertEquals(TEXT(-12.3, "000.00"), "-012.30");
1379+ assertEquals(TEXT(-12.3, "+-$%000.0"), "-+-$%012.3");
1380+ assertEquals(TEXT(-12.3, "+-$%00.0"), "-+-$%12.3");
1381+ assertEquals(TEXT(-12.3, "+-$%00.0+-$"), "-+-$%12.3+-$");
1382+ assertEquals(TEXT(12.3, "000000000000000.0000000000000"), "000000000000012.3000000000000");
1383+ assertEquals(TEXT(12.33, "000000000000000.0000000000000"), "000000000000012.3300000000000");
1384+ assertEquals(TEXT(12.33555, "000000000000000.0000000000000"), "000000000000012.3355500000000");
1385+ assertEquals(TEXT(12.33555, "000000000000000.0000"), "000000000000012.3356");
1386+ assertEquals(TEXT(12.3, "+-$%000.0"), "+-$%012.3");
1387+ assertEquals(TEXT(12.3, "+-$%00.0"), "+-$%12.3");
1388+ assertEquals(TEXT(12.3, "+-$%00.0+-$"), "+-$%12.3+-$");
1389+ assertEquals(TEXT(12, "0,000.0"), "0,012.0");
1390+ assertEquals(TEXT(123342424, "0000000,000.0"), "0,123,342,424.0");
1391+ assertEquals(TEXT(0.01, "00.0"), "00.0");
1392+ assertEquals(TEXT(0.01, "00.0"), "00.0");
1393+ assertEquals(TEXT(0.01, "00.0"), "00.0");
1394+ assertEquals(TEXT(12, "00"), "12");
1395+ assertEquals(TEXT(12.3, "00"), "12");
1396+ assertEquals(TEXT(12, "00.00"), "12.00");
1397+ assertEquals(TEXT(0.99, "0.00"), "0.99");
1398+ assertEquals(TEXT(0.99, ".00"), ".99");
1399+ assertEquals(TEXT(0.99, "00.00"), "00.99");
1400+ assertEquals(TEXT(0.99, "0000.00"), "0000.99");
1401+ assertEquals(TEXT(0.99, "0.0000"), "0.9900");
1402+ assertEquals(TEXT(0.99, "0.0"), "1.0");
1403+ assertEquals(TEXT(0.88, "0.0"), "0.9");
1404+ assertEquals(TEXT(0.88, "00.0"), "00.9");
1405+ assertEquals(TEXT(0.88, "00.00"), "00.88");
1406+ assertEquals(TEXT(0.88, "00.000"), "00.880");
1407+ assertEquals(TEXT(1.88, "00.000"), "01.880");
1408+ assertEquals(TEXT(1.99, "00.0"), "02.0");
1409+ assertEquals(TEXT(1234223.3324224, "0000000000.0"), "0001234223.3");
1410+ assertEquals(TEXT(12, "%00%"), "%12%");
1411+ assertEquals(TEXT(12.3, "00.0%"), "12.3%");
1412+ assertEquals(TEXT(12.3, "$+-00.0$+-"), "$+-12.3$+-");
1413+ assertEquals(TEXT(123456789, "0,0.0"), "123,456,789.0");
1414+ assertEquals(TEXT(123456789.99, "0,0.0"), "123,456,790.0");
1415+ assertEquals(TEXT(0.99, "0,000.00"), "0,000.99");
1416+ assertEquals(TEXT(0.99, "0,000.0"), "0,001.0");
1417+ catchAndAssertEquals(function() {
1418+ TEXT(0.99, "0.00#");
1419+ }, ERRORS.VALUE_ERROR);
1420+ catchAndAssertEquals(function() {
1421+ TEXT(0.99, "#0.00");
1422+ }, ERRORS.VALUE_ERROR);
1423+ catchAndAssertEquals(function() {
1424+ TEXT.apply(this, [100])
1425+ }, ERRORS.NA_ERROR);
1426+ catchAndAssertEquals(function() {
1427+ TEXT.apply(this, [100, "0", 10])
1428+ }, ERRORS.NA_ERROR);
1429 });
1430diff --git a/tests/SheetFormulaTest.ts b/tests/SheetFormulaTest.ts
1431index 600804f..1b7a760 100644
1432--- a/tests/SheetFormulaTest.ts
1433+++ b/tests/SheetFormulaTest.ts
1434@@ -1017,6 +1017,10 @@ test("Sheet ROMAN", function(){
1435 assertFormulaEquals('=ROMAN(3999)', "MMMCMXCIX");
1436 });
1437
1438+test("Sheet TEXT", function(){
1439+ assertFormulaEquals('=TEXT(12.3, "###.##")', "12.3");
1440+});
1441+
1442 test("Sheet parsing error", function(){
1443 assertFormulaEqualsError('= 10e', PARSE_ERROR);
1444 assertFormulaEqualsError('= SUM(', PARSE_ERROR);
1445@@ -1028,6 +1032,11 @@ test("Sheet *", function(){
1446 assertFormulaEquals('= 1 * 1', 1);
1447 });
1448
1449+test("Sheet &", function(){
1450+ assertFormulaEquals('="hey"&" "&"there"', "hey there");
1451+ assertFormulaEquals('=TEXT(12.3, "###.##")&"mm"', "12.3mm");
1452+});
1453+
1454 test("Sheet /", function(){
1455 assertFormulaEquals('= 10 / 2', 5);
1456 assertFormulaEquals('= 10 / 1', 10);
1457diff --git a/tests/Utilities/MoreUtilsTest.ts b/tests/Utilities/MoreUtilsTest.ts
1458new file mode 100644
1459index 0000000..0e91c61
1460--- /dev/null
1461+++ b/tests/Utilities/MoreUtilsTest.ts
1462@@ -0,0 +1,80 @@
1463+import {
1464+ assertEquals,
1465+ test
1466+} from "../Utils/Asserts";
1467+import {
1468+ isDefined,
1469+ isUndefined,
1470+ NumberStringBuilder
1471+} from "../../src/Utilities/MoreUtils";
1472+
1473+test("MoreUtils.isDefined", function () {
1474+ let und;
1475+ assertEquals(isDefined(und), false);
1476+ assertEquals(isDefined("10"), true);
1477+ assertEquals(isDefined(10), true);
1478+ assertEquals(isDefined(true), true);
1479+ assertEquals(isDefined(false), true);
1480+});
1481+
1482+test("MoreUtils.isUndefined", function () {
1483+ let und;
1484+ assertEquals(isUndefined(und), true);
1485+ assertEquals(isUndefined("10"), false);
1486+ assertEquals(isUndefined(10), false);
1487+ assertEquals(isUndefined(true), false);
1488+ assertEquals(isUndefined(false), false);
1489+});
1490+
1491+test("MoreUtils.NumberStringBuilder", function () {
1492+ assertEquals(NumberStringBuilder.start().number(12.3).integerZeros(2).decimalZeros(1).build(), "12.3");
1493+ assertEquals(NumberStringBuilder.start().number(0.01).integerZeros(2).decimalZeros(1).build(), "00.0");
1494+ assertEquals(NumberStringBuilder.start().number(12).integerZeros(2).decimalZeros(0).build(), "12");
1495+ assertEquals(NumberStringBuilder.start().number(12.3).integerZeros(2).decimalZeros(0).build(), "12");
1496+ assertEquals(NumberStringBuilder.start().number(12).integerZeros(2).decimalZeros(2).build(), "12.00");
1497+ assertEquals(NumberStringBuilder.start().number(0.99).integerZeros(1).decimalZeros(2).build(), "0.99");
1498+ assertEquals(NumberStringBuilder.start().number(0.99).integerZeros(2).decimalZeros(2).build(), "00.99");
1499+ assertEquals(NumberStringBuilder.start().number(0.99).integerZeros(4).decimalZeros(2).build(), "0000.99");
1500+ assertEquals(NumberStringBuilder.start().number(0.99).integerZeros(1).decimalZeros(4).build(), "0.9900");
1501+ assertEquals(NumberStringBuilder.start().number(0.99).integerZeros(1).decimalZeros(1).build(), "1.0");
1502+ assertEquals(NumberStringBuilder.start().number(0.88).integerZeros(1).decimalZeros(1).build(), "0.9");
1503+ assertEquals(NumberStringBuilder.start().number(0.99).integerZeros(0).decimalZeros(2).build(), ".99");
1504+ assertEquals(NumberStringBuilder.start().number(0.88).integerZeros(2).decimalZeros(1).build(), "00.9");
1505+ assertEquals(NumberStringBuilder.start().number(0.88).integerZeros(2).decimalZeros(2).build(), "00.88");
1506+ assertEquals(NumberStringBuilder.start().number(0.88).integerZeros(2).decimalZeros(3).build(), "00.880");
1507+ assertEquals(NumberStringBuilder.start().number(1.88).integerZeros(2).decimalZeros(3).build(), "01.880");
1508+ assertEquals(NumberStringBuilder.start().number(1.99).integerZeros(2).decimalZeros(1).build(), "02.0");
1509+
1510+ assertEquals(NumberStringBuilder.start().number(-12.3).integerZeros(2).decimalZeros(1).build(), "-12.3");
1511+ assertEquals(NumberStringBuilder.start().number(-0.01).integerZeros(2).decimalZeros(1).build(), "-00.0");
1512+ assertEquals(NumberStringBuilder.start().number(-12).integerZeros(2).decimalZeros(0).build(), "-12");
1513+ assertEquals(NumberStringBuilder.start().number(-12.3).integerZeros(2).decimalZeros(0).build(), "-12");
1514+ assertEquals(NumberStringBuilder.start().number(-12).integerZeros(2).decimalZeros(2).build(), "-12.00");
1515+ assertEquals(NumberStringBuilder.start().number(-0.99).integerZeros(1).decimalZeros(2).build(), "-0.99");
1516+ assertEquals(NumberStringBuilder.start().number(-0.99).integerZeros(2).decimalZeros(2).build(), "-00.99");
1517+ assertEquals(NumberStringBuilder.start().number(-0.99).integerZeros(4).decimalZeros(2).build(), "-0000.99");
1518+ assertEquals(NumberStringBuilder.start().number(-0.99).integerZeros(1).decimalZeros(4).build(), "-0.9900");
1519+ assertEquals(NumberStringBuilder.start().number(-0.99).integerZeros(1).decimalZeros(1).build(), "-1.0");
1520+ assertEquals(NumberStringBuilder.start().number(-0.88).integerZeros(1).decimalZeros(1).build(), "-0.9");
1521+ assertEquals(NumberStringBuilder.start().number(-0.99).integerZeros(0).decimalZeros(2).build(), "-.99");
1522+ assertEquals(NumberStringBuilder.start().number(-0.88).integerZeros(2).decimalZeros(1).build(), "-00.9");
1523+ assertEquals(NumberStringBuilder.start().number(-0.88).integerZeros(2).decimalZeros(2).build(), "-00.88");
1524+ assertEquals(NumberStringBuilder.start().number(-0.88).integerZeros(2).decimalZeros(3).build(), "-00.880");
1525+ assertEquals(NumberStringBuilder.start().number(-1.88).integerZeros(2).decimalZeros(3).build(), "-01.880");
1526+ assertEquals(NumberStringBuilder.start().number(-1.99).integerZeros(2).decimalZeros(1).build(), "-02.0");
1527+ assertEquals(NumberStringBuilder.start().number(-12).integerZeros(2).decimalZeros(0).tail("%").head("%").build(), "-%12%");
1528+
1529+
1530+ assertEquals(NumberStringBuilder.start().number(1234223.3324224).integerZeros(10).decimalZeros(1).build(), "0001234223.3");
1531+ assertEquals(NumberStringBuilder.start().number(12).integerZeros(2).decimalZeros(0).tail("%").head("%").build(), "%12%");
1532+ assertEquals(NumberStringBuilder.start().number(12.3).integerZeros(2).decimalZeros(1).tail("%").build(), "12.3%");
1533+ assertEquals(NumberStringBuilder.start().number(12.3).integerZeros(2).decimalZeros(1).head("%").build(), "%12.3");
1534+ assertEquals(NumberStringBuilder.start().number(12.3).integerZeros(2).decimalZeros(1).head("$+_").tail("$+_").build(), "$+_12.3$+_");
1535+ assertEquals(NumberStringBuilder.start().number(123456789).integerZeros(1).decimalZeros(1).commafy(true).build(), "123,456,789.0");
1536+ assertEquals(NumberStringBuilder.start().number(123456789.99).integerZeros(1).decimalZeros(1).commafy(true).build(), "123,456,790.0");
1537+ assertEquals(NumberStringBuilder.start().number(0.99).integerZeros(4).decimalZeros(2).commafy(true).build(), "0,000.99");
1538+
1539+ assertEquals(NumberStringBuilder.start().number(12.3).integerZeros(1).maximumDecimalPlaces(1).build(), "12.3");
1540+ assertEquals(NumberStringBuilder.start().number(12.33333).integerZeros(1).maximumDecimalPlaces(100).build(), "12.33333");
1541+ assertEquals(NumberStringBuilder.start().number(12.33).integerZeros(1).maximumDecimalPlaces(100).build(), "12.33");
1542+});
1543\ No newline at end of file
1544diff --git a/tests/Utilities/TypeConverterTest.ts b/tests/Utilities/TypeConverterTest.ts
1545index 527b085..72f7a75 100644
1546--- a/tests/Utilities/TypeConverterTest.ts
1547+++ b/tests/Utilities/TypeConverterTest.ts
1548@@ -3,7 +3,7 @@ import * as moment from "moment";
1549 import {
1550 assertEquals,
1551 test,
1552- catchAndAssertEquals
1553+ catchAndAssertEquals, lockDate
1554 } from "../Utils/Asserts";
1555 import {
1556 TypeConverter
1557@@ -16,7 +16,7 @@ import {
1558 Cell
1559 } from "../../src/Cell";
1560
1561-var ERROR_CELL = new Cell("A1");
1562+let ERROR_CELL = new Cell("A1");
1563 ERROR_CELL.setError(new ValueError("Whooops!"));
1564
1565
1566@@ -935,6 +935,38 @@ test("TypeConverter.stringToDateNumber", function () {
1567 catchAndAssertEquals(function() {
1568 TypeConverter.stringToDateNumber("January-2000 100000000:100000000:1001000000");
1569 }, VALUE_ERROR);
1570+ // MONTHDIG_DAYDIG, MM(fd)DD, '09/01' =========================================================================
1571+ lockDate(2017, 9, 24, 10, 55, 23);
1572+ assertEquals(TypeConverter.stringToDateNumber("01/09"), 42744);
1573+ assertEquals(TypeConverter.stringToDateNumber("02/09"), 42775);
1574+ assertEquals(TypeConverter.stringToDateNumber("03/09"), 42803);
1575+ assertEquals(TypeConverter.stringToDateNumber("04/09"), 42834);
1576+ assertEquals(TypeConverter.stringToDateNumber("05/09"), 42864);
1577+ assertEquals(TypeConverter.stringToDateNumber("06/09"), 42895);
1578+ assertEquals(TypeConverter.stringToDateNumber("07/09"), 42925);
1579+ assertEquals(TypeConverter.stringToDateNumber("08/09"), 42956);
1580+ assertEquals(TypeConverter.stringToDateNumber("09/09"), 42987);
1581+ assertEquals(TypeConverter.stringToDateNumber("10/09"), 43017);
1582+ assertEquals(TypeConverter.stringToDateNumber("11/09"), 43048);
1583+ assertEquals(TypeConverter.stringToDateNumber("12/09"), 43078);
1584+ assertEquals(TypeConverter.stringToDateNumber("01/01"), 42736);
1585+ assertEquals(TypeConverter.stringToDateNumber("01/02"), 42737);
1586+ assertEquals(TypeConverter.stringToDateNumber("01/03"), 42738);
1587+ assertEquals(TypeConverter.stringToDateNumber("01/04"), 42739);
1588+ assertEquals(TypeConverter.stringToDateNumber("01/05"), 42740);
1589+ assertEquals(TypeConverter.stringToDateNumber("01/29"), 42764);
1590+ assertEquals(TypeConverter.stringToDateNumber("01/30"), 42765);
1591+ assertEquals(TypeConverter.stringToDateNumber("01/31"), 42766);
1592+ assertEquals(TypeConverter.stringToDateNumber("01/09 10:10:10am"), 42744);
1593+ assertEquals(TypeConverter.stringToDateNumber("01/09 10:10:100000"), 42745);
1594+ assertEquals(TypeConverter.stringToDateNumber("08/09 10:10:100000"), 42957);
1595+ assertEquals(TypeConverter.stringToDateNumber("01/02 10am"), 42737);
1596+ assertEquals(TypeConverter.stringToDateNumber("01/02 10:10"), 42737);
1597+ assertEquals(TypeConverter.stringToDateNumber("01/02 10:10am"), 42737);
1598+ assertEquals(TypeConverter.stringToDateNumber("01/02 10:10:10"), 42737);
1599+ assertEquals(TypeConverter.stringToDateNumber("01/02 10:10:10am"), 42737);
1600+ assertEquals(TypeConverter.stringToDateNumber("01/02 10 am"), 42737);
1601+ assertEquals(TypeConverter.stringToDateNumber("01/02 10: 10: 10 am "), 42737);
1602 });
1603
1604
1605diff --git a/tests/Utils/Asserts.ts b/tests/Utils/Asserts.ts
1606index efd274f..e49fdd7 100644
1607--- a/tests/Utils/Asserts.ts
1608+++ b/tests/Utils/Asserts.ts
1609@@ -132,6 +132,17 @@ function assertFormulaEqualsArray(formula: string, expectation: any) {
1610 }
1611 }
1612
1613+// WARNING: Locking in Date by overriding prototypes.
1614+function lockDate(year, month, day, hour, minute, second) {
1615+ let d = new Date(year, month, day, hour, minute, second);
1616+ Date.prototype.constructor = function () {
1617+ return d;
1618+ };
1619+ Date.now = function () {
1620+ return +(d);
1621+ };
1622+}
1623+
1624
1625 export {
1626 assertIsNull,
1627@@ -143,5 +154,6 @@ export {
1628 assertFormulaEqualsError,
1629 assertFormulaEqualsDependsOnReference,
1630 catchAndAssertEquals,
1631- test
1632+ test,
1633+ lockDate
1634 }
1635\ No newline at end of file
1636diff --git a/tsconfig.json b/tsconfig.json
1637index 9f9c924..7396082 100644
1638--- a/tsconfig.json
1639+++ b/tsconfig.json
1640@@ -3,7 +3,8 @@
1641 "allowJs": true,
1642 "allowUnreachableCode": true,
1643 "allowUnusedLabels": true,
1644- "outDir": "dist"
1645+ "outDir": "dist",
1646+ "sourceMap": false
1647 },
1648 "files": [
1649 "src/Sheet.ts"