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("../index.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