spreadsheet
typeScript/javascript spreadsheet parser, with formulas.
git clone https://git.vogt.world/spreadsheet.git
Log | Files | README.md
← Commit log
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"