1 /* 2 * INumber.js - Parse a number in any locale 3 * 4 * Copyright © 2012-2015, JEDLSoft 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 20 var ilib = require("./ilib.js"); 21 22 var Locale = require("./Locale.js"); 23 var LocaleInfo = require("./LocaleInfo.js"); 24 25 var isDigit = require("./isDigit.js"); 26 var isSpace = require("./isSpace.js"); 27 28 var Currency = require("./Currency.js"); 29 30 31 /** 32 * @class 33 * Parse a string as a number, ignoring all locale-specific formatting.<p> 34 * 35 * This class is different from the standard Javascript parseInt() and parseFloat() 36 * functions in that the number to be parsed can have formatting characters in it 37 * that are not supported by those two 38 * functions, and it handles numbers written in other locales properly. For example, 39 * if you pass the string "203,231.23" to the parseFloat() function in Javascript, it 40 * will return you the number 203. The INumber class will parse it correctly and 41 * the value() function will return the number 203231.23. If you pass parseFloat() the 42 * string "203.231,23" with the locale set to de-DE, it will return you 203 again. This 43 * class will return the correct number 203231.23 again.<p> 44 * 45 * The options object may contain any of the following properties: 46 * 47 * <ul> 48 * <li><i>locale</i> - specify the locale of the string to parse. This is used to 49 * figure out what the decimal point character is. If not specified, the default locale 50 * for the app or browser is used. 51 * <li><i>type</i> - specify whether this string should be interpretted as a number, 52 * currency, or percentage amount. When the number is interpretted as a currency 53 * amount, the getCurrency() method will return something useful, otherwise it will 54 * return undefined. If 55 * the number is to be interpretted as percentage amount and there is a percentage sign 56 * in the string, then the number will be returned 57 * as a fraction from the valueOf() method. If there is no percentage sign, then the 58 * number will be returned as a regular number. That is "58.3%" will be returned as the 59 * number 0.583 but "58.3" will be returned as 58.3. Valid values for this property 60 * are "number", "currency", and "percentage". Default if this is not specified is 61 * "number". 62 * <li><i>onLoad</i> - a callback function to call when the locale data is fully 63 * loaded. When the onLoad option is given, this class will attempt to 64 * load any missing locale data using the ilib loader callback. 65 * When the constructor is done (even if the data is already preassembled), the 66 * onLoad function is called with the current instance as a parameter, so this 67 * callback can be used with preassembled or dynamic loading or a mix of the two. 68 * 69 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or 70 * asynchronously. If this option is given as "false", then the "onLoad" 71 * callback must be given, as the instance returned from this constructor will 72 * not be usable for a while. 73 * 74 * <li><i>loadParams</i> - an object containing parameters to pass to the 75 * loader callback function when locale data is missing. The parameters are not 76 * interpretted or modified in any way. They are simply passed along. The object 77 * may contain any property/value pairs as long as the calling code is in 78 * agreement with the loader callback function as to what those parameters mean. 79 * </ul> 80 * <p> 81 * 82 * This class is named INumber ("ilib number") so as not to conflict with the 83 * built-in Javascript Number class. 84 * 85 * @constructor 86 * @param {string|number|INumber|Number|undefined} str a string to parse as a number, or a number value 87 * @param {Object=} options Options controlling how the instance should be created 88 */ 89 var INumber = function (str, options) { 90 var i, stripped = "", 91 sync = true; 92 93 this.locale = new Locale(); 94 this.type = "number"; 95 96 if (options) { 97 if (options.locale) { 98 this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale; 99 } 100 if (options.type) { 101 switch (options.type) { 102 case "number": 103 case "currency": 104 case "percentage": 105 this.type = options.type; 106 break; 107 default: 108 break; 109 } 110 } 111 if (typeof(options.sync) !== 'undefined') { 112 sync = !!options.sync; 113 } 114 } else { 115 options = {sync: true}; 116 } 117 118 isDigit._init(sync, options.loadParams, ilib.bind(this, function() { 119 isSpace._init(sync, options.loadParams, ilib.bind(this, function() { 120 new LocaleInfo(this.locale, { 121 sync: sync, 122 loadParams: options.loadParams, 123 onLoad: ilib.bind(this, function (li) { 124 this.li = li; 125 this.decimal = li.getDecimalSeparator(); 126 var nativeDecimal = this.li.getNativeDecimalSeparator() || ""; 127 128 switch (typeof(str)) { 129 case 'string': 130 // stripping should work for all locales, because you just ignore all the 131 // formatting except the decimal char 132 var unary = true; // looking for the unary minus still? 133 var lastNumericChar = 0; 134 this.str = str || "0"; 135 i = 0; 136 for (i = 0; i < this.str.length; i++) { 137 if (unary && this.str.charAt(i) === '-') { 138 unary = false; 139 stripped += this.str.charAt(i); 140 lastNumericChar = i; 141 } else if (isDigit(this.str.charAt(i))) { 142 stripped += this.str.charAt(i); 143 unary = false; 144 lastNumericChar = i; 145 } else if (this.str.charAt(i) === this.decimal || this.str.charAt(i) === nativeDecimal) { 146 stripped += "."; // always convert to period 147 unary = false; 148 lastNumericChar = i; 149 } // else ignore 150 } 151 // record what we actually parsed 152 this.parsed = this.str.substring(0, lastNumericChar+1); 153 154 /** @type {number} */ 155 this.value = parseFloat(this._mapToLatinDigits(stripped)); 156 break; 157 case 'number': 158 this.str = "" + str; 159 this.value = str; 160 break; 161 162 case 'object': 163 // call parseFloat to coerse the type to number 164 this.value = parseFloat(str.valueOf()); 165 this.str = "" + this.value; 166 break; 167 168 case 'undefined': 169 this.value = 0; 170 this.str = "0"; 171 break; 172 } 173 174 switch (this.type) { 175 default: 176 // don't need to do anything special for other types 177 break; 178 case "percentage": 179 if (this.str.indexOf(li.getPercentageSymbol()) !== -1) { 180 this.value /= 100; 181 } 182 break; 183 case "currency": 184 stripped = ""; 185 i = 0; 186 while (i < this.str.length && 187 !isDigit(this.str.charAt(i)) && 188 !isSpace(this.str.charAt(i))) { 189 stripped += this.str.charAt(i++); 190 } 191 if (stripped.length === 0) { 192 while (i < this.str.length && 193 isDigit(this.str.charAt(i)) || 194 isSpace(this.str.charAt(i)) || 195 this.str.charAt(i) === '.' || 196 this.str.charAt(i) === ',' ) { 197 i++; 198 } 199 while (i < this.str.length && 200 !isDigit(this.str.charAt(i)) && 201 !isSpace(this.str.charAt(i))) { 202 stripped += this.str.charAt(i++); 203 } 204 } 205 new Currency({ 206 locale: this.locale, 207 sign: stripped, 208 sync: sync, 209 loadParams: options.loadParams, 210 onLoad: ilib.bind(this, function (cur) { 211 this.currency = cur; 212 if (options && typeof(options.onLoad) === 'function') { 213 options.onLoad(this); 214 } 215 }) 216 }); 217 return; 218 } 219 220 if (options && typeof(options.onLoad) === 'function') { 221 options.onLoad(this); 222 } 223 }) 224 }); 225 })); 226 })); 227 }; 228 229 INumber.prototype = { 230 /** 231 * @private 232 */ 233 _mapToLatinDigits: function(str) { 234 // only map if there are actual native digits 235 var digits = this.li.getNativeDigits(); 236 if (!digits) return str; 237 238 var digitMap = {}; 239 for (var i = 0; i < digits.length; i++) { 240 digitMap[digits[i]] = String(i); 241 } 242 var decimal = this.li.getNativeDecimalSeparator(); 243 244 return str.split("").map(function(ch) { 245 if (ch == decimal) return "."; 246 return digitMap[ch] || ch; 247 }).join(""); 248 }, 249 250 /** 251 * Return the locale for this formatter instance. 252 * @return {Locale} the locale instance for this formatter 253 */ 254 getLocale: function () { 255 return this.locale; 256 }, 257 258 /** 259 * Return the original string that this number instance was created with. 260 * @return {string} the original string 261 */ 262 toString: function () { 263 return this.str; 264 }, 265 266 /** 267 * If the type of this INumber instance is "currency", then the parser will attempt 268 * to figure out which currency this amount represents. The amount can be written 269 * with any of the currency signs or ISO 4217 codes that are currently 270 * recognized by ilib, and the currency signs may occur before or after the 271 * numeric portion of the string. If no currency can be recognized, then the 272 * default currency for the locale is returned. If multiple currencies can be 273 * recognized (for example if the currency sign is "$"), then this method 274 * will prefer the one for the current locale. If multiple currencies can be 275 * recognized, but none are used in the current locale, then the first currency 276 * encountered will be used. This may produce random results, though the larger 277 * currencies occur earlier in the list. For example, if the sign found in the 278 * string is "$" and that is not the sign of the currency of the current locale 279 * then the US dollar will be recognized, as it is the largest currency that uses 280 * the "$" as its sign. 281 * 282 * @return {Currency|undefined} the currency instance for this amount, or 283 * undefined if this INumber object is not of type currency 284 */ 285 getCurrency: function () { 286 return this.currency; 287 }, 288 289 /** 290 * Return the value of this INumber object as a primitive number instance. 291 * @return {number} the value of this number instance 292 */ 293 valueOf: function () { 294 return this.value; 295 } 296 }; 297 298 module.exports = INumber; 299