1 /*
  2  * phonefmt.js - Represent a phone number formatter.
  3  *
  4  * Copyright © 2014-2015, 2018, 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 // !data phonefmt
 21 
 22 var ilib = require("./ilib.js");
 23 var Utils = require("./Utils.js");
 24 var JSUtils = require("./JSUtils.js");
 25 var Locale = require("./Locale.js");
 26 var PhoneNumber = require("./PhoneNumber.js");
 27 var NumberingPlan = require("./NumberingPlan.js");
 28 var PhoneLocale = require("./PhoneLocale.js");
 29 
 30 /**
 31  * @class
 32  * Create a new phone number formatter object that formats numbers according to the parameters.<p>
 33  *
 34  * The options object can contain zero or more of the following parameters:
 35  *
 36  * <ul>
 37  * <li><i>locale</i> locale to use to format this number, or undefined to use the default locale
 38  * <li><i>style</i> the name of style to use to format numbers, or undefined to use the default style
 39  * <li><i>mcc</i> the MCC of the country to use if the number is a local number and the country code is not known
 40  *
 41  * <li><i>onLoad</i> - a callback function to call when the locale data is fully loaded and the address has been
 42  * parsed. When the onLoad option is given, the address formatter object
 43  * will attempt to load any missing locale data using the ilib loader callback.
 44  * When the constructor is done (even if the data is already preassembled), the
 45  * onLoad function is called with the current instance as a parameter, so this
 46  * callback can be used with preassembled or dynamic loading or a mix of the two.
 47  *
 48  * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
 49  * asynchronously. If this option is given as "false", then the "onLoad"
 50  * callback must be given, as the instance returned from this constructor will
 51  * not be usable for a while.
 52  *
 53  * <li><i>loadParams</i> - an object containing parameters to pass to the
 54  * loader callback function when locale data is missing. The parameters are not
 55  * interpretted or modified in any way. They are simply passed along. The object
 56  * may contain any property/value pairs as long as the calling code is in
 57  * agreement with the loader callback function as to what those parameters mean.
 58  * </ul>
 59  *
 60  * Some regions have more than one style of formatting, and the style parameter
 61  * selects which style the user prefers. An array of style names that this locale
 62  * supports can be found by calling {@link PhoneFmt.getAvailableStyles}.
 63  * Example phone numbers can be retrieved for each style by calling
 64  * {@link PhoneFmt.getStyleExample}.
 65  * <p>
 66  *
 67  * If the MCC is given, numbers will be formatted in the manner of the country
 68  * specified by the MCC. If it is not given, but the locale is, the manner of
 69  * the country in the locale will be used. If neither the locale or MCC are not given,
 70  * then the country of the current ilib locale is used.
 71  *
 72  * @constructor
 73  * @param {Object} options properties that control how this formatter behaves
 74  */
 75 var PhoneFmt = function(options) {
 76     this.sync = true;
 77     this.styleName = 'default',
 78     this.loadParams = {};
 79 
 80     var locale = new Locale();
 81 
 82     if (options) {
 83         if (options.locale) {
 84             locale = options.locale;
 85         }
 86 
 87         if (typeof(options.sync) !== 'undefined') {
 88             this.sync = !!options.sync;
 89         }
 90 
 91         if (options.loadParams) {
 92             this.loadParams = options.loadParams;
 93         }
 94 
 95         if (options.style) {
 96             this.style = options.style;
 97         }
 98     }
 99 
100     new PhoneLocale({
101         locale: locale,
102         mcc: options && options.mcc,
103         countryCode: options && options.countryCode,
104         onLoad: ilib.bind(this, function (data) {
105             /** @type {PhoneLocale} */
106             this.locale = data;
107 
108             new NumberingPlan({
109                 locale: this.locale,
110                 sync: this.sync,
111                 loadParms: this.loadParams,
112                 onLoad: ilib.bind(this, function (plan) {
113                     /** @type {NumberingPlan} */
114                     this.plan = plan;
115 
116                     Utils.loadData({
117                         name: "phonefmt.json",
118                         object: "PhoneFmt",
119                         locale: this.locale,
120                         sync: this.sync,
121                         loadParams: JSUtils.merge(this.loadParams, {
122                             returnOne: true
123                         }),
124                         callback: ilib.bind(this, function (fmtdata) {
125                             this.fmtdata = fmtdata;
126 
127                             if (options && typeof(options.onLoad) === 'function') {
128                                 options.onLoad(this);
129                             }
130                         })
131                     });
132                 })
133             });
134         })
135     });
136 };
137 
138 PhoneFmt.prototype = {
139     /**
140      *
141      * @protected
142      * @param {string} part
143      * @param {Object} formats
144      * @param {boolean} mustUseAll
145      */
146     _substituteDigits: function(part, formats, mustUseAll) {
147         var formatString,
148             formatted = "",
149             partIndex = 0,
150             templates,
151             i;
152 
153         // console.info("Globalization.Phone._substituteDigits: typeof(formats) is " + typeof(formats));
154         if (!part) {
155             return formatted;
156         }
157 
158         if (typeof(formats) === "object") {
159             templates = (typeof(formats.template) !== 'undefined') ? formats.template : formats;
160             if (part.length > templates.length) {
161                 // too big, so just use last resort rule.
162                 throw "part " + part + " is too big. We do not have a format template to format it.";
163             }
164             // use the format in this array that corresponds to the digit length of this
165             // part of the phone number
166             formatString =  templates[part.length-1];
167             // console.info("Globalization.Phone._substituteDigits: formats is an Array: " + JSON.stringify(formats));
168         } else {
169             formatString = formats;
170         }
171 
172         for (i = 0; i < formatString.length; i++) {
173             if (formatString.charAt(i) === "X") {
174                 formatted += part.charAt(partIndex);
175                 partIndex++;
176             } else {
177                 formatted += formatString.charAt(i);
178             }
179         }
180 
181         if (mustUseAll && partIndex < part.length-1) {
182             // didn't use the whole thing in this format? Hmm... go to last resort rule
183             throw "too many digits in " + part + " for format " + formatString;
184         }
185 
186         return formatted;
187     },
188 
189     /**
190      * Returns the style with the given name, or the default style if there
191      * is no style with that name.
192      * @protected
193      * @return {{example:string,whole:Object.<string,string>,partial:Object.<string,string>}|Object.<string,string>}
194      */
195     _getStyle: function (name, fmtdata) {
196         return fmtdata[name] || fmtdata["default"];
197     },
198 
199     /**
200      * Do the actual work of formatting the phone number starting at the given
201      * field in the regular field order.
202      *
203      * @param {!PhoneNumber} number
204      * @param {{
205      *   partial:boolean,
206      *   style:string,
207      *   mcc:string,
208      *   locale:(string|Locale),
209      *   sync:boolean,
210      *   loadParams:Object,
211      *   onLoad:function(string)
212      * }} options Parameters which control how to format the number
213      * @param {number} startField
214      */
215     _doFormat: function(number, options, startField, locale, fmtdata, callback) {
216         var sync = true,
217             loadParams = {},
218             temp,
219             templates,
220             fieldName,
221             countryCode,
222             isWhole,
223             style,
224             formatted = "",
225             styleTemplates,
226             lastFieldName;
227 
228         if (options) {
229             if (typeof(options.sync) !== 'undefined') {
230                 sync = !!options.sync;
231             }
232 
233             if (options.loadParams) {
234                 loadParams = options.loadParams;
235             }
236         }
237 
238         style = this.style; // default style for this formatter
239 
240         // figure out what style to use for this type of number
241         if (number.countryCode) {
242             // dialing from outside the country
243             // check to see if it to a mobile number because they are often formatted differently
244             style = (number.mobilePrefix) ? "internationalmobile" : "international";
245         } else if (number.mobilePrefix !== undefined) {
246             style = "mobile";
247         } else if (number.serviceCode !== undefined && typeof(fmtdata["service"]) !== 'undefined') {
248             // if there is a special format for service numbers, then use it
249             style = "service";
250         }
251 
252         isWhole = (!options || !options.partial);
253         styleTemplates = this._getStyle(style, fmtdata);
254 
255         // console.log("Style ends up being " + style + " and using subtype " + (isWhole ? "whole" : "partial"));
256         styleTemplates = (isWhole ? styleTemplates.whole : styleTemplates.partial) || styleTemplates;
257 
258         for (var i = startField; i < PhoneNumber._fieldOrder.length; i++) {
259             fieldName = PhoneNumber._fieldOrder[i];
260             // console.info("format: formatting field " + fieldName + " value: " + number[fieldName]);
261             if (number[fieldName] !== undefined) {
262                 if (styleTemplates[fieldName] !== undefined) {
263                     templates = styleTemplates[fieldName];
264                     if (fieldName === "trunkAccess") {
265                         if (number.areaCode === undefined && number.serviceCode === undefined && number.mobilePrefix === undefined) {
266                             templates = "X";
267                         }
268                     }
269                     if (lastFieldName && typeof(styleTemplates[lastFieldName].suffix) !== 'undefined') {
270                         if (fieldName !== "extension" && number[fieldName].search(/[xwtp,;]/i) <= -1) {
271                             formatted += styleTemplates[lastFieldName].suffix;
272                         }
273                     }
274                     lastFieldName = fieldName;
275 
276                     // console.info("format: formatting field " + fieldName + " with templates " + JSON.stringify(templates));
277                     temp = this._substituteDigits(number[fieldName], templates, (fieldName === "subscriberNumber"));
278                     // console.info("format: formatted is: " + temp);
279                     formatted += temp;
280 
281                     if (fieldName === "countryCode") {
282                         // switch to the new country to format the rest of the number
283                         countryCode = number.countryCode.replace(/[wWpPtT\+#\*]/g, '');    // fix for NOV-108200
284 
285                         new PhoneLocale({
286                             locale: this.locale,
287                             sync: sync,
288                             loadParms: loadParams,
289                             countryCode: countryCode,
290                             onLoad: ilib.bind(this, function (locale) {
291                                 Utils.loadData({
292                                     name: "phonefmt.json",
293                                     object: "PhoneFmt",
294                                     locale: locale,
295                                     sync: sync,
296                                     loadParams: JSUtils.merge(loadParams, {
297                                         returnOne: true
298                                     }),
299                                     callback: ilib.bind(this, function (fmtdata) {
300                                         // console.info("format: switching to region " + locale.region + " and style " + style + " to format the rest of the number ");
301 
302                                         var subfmt = "";
303 
304                                         this._doFormat(number, options, i+1, locale, fmtdata, function (subformat) {
305                                             subfmt = subformat;
306                                             if (typeof(callback) === 'function') {
307                                                 callback(formatted + subformat);
308                                             }
309                                         });
310 
311                                         formatted += subfmt;
312                                     })
313                                 });
314                             })
315                         });
316                         return formatted;
317                     }
318                 } else {
319                     //console.warn("PhoneFmt.format: cannot find format template for field " + fieldName + ", region " + locale.region + ", style " + style);
320                     // use default of "minimal formatting" so we don't miss parts because of bugs in the format templates
321                     formatted += number[fieldName];
322                 }
323             }
324         }
325 
326         if (typeof(callback) === 'function') {
327             callback(formatted);
328         }
329 
330         return formatted;
331     },
332 
333     /**
334      * Format the parts of a phone number appropriately according to the settings in
335      * this formatter instance.
336      *
337      * The options can contain zero or more of these properties:
338      *
339      * <ul>
340      * <li><i>partial</i> boolean which tells whether or not this phone number
341      * represents a partial number or not. The default is false, which means the number
342      * represents a whole number.
343      * <li><i>style</i> style to use to format the number, if different from the
344      * default style or the style specified in the constructor
345      * <li><i>locale</i> The locale with which to parse the number. This gives a clue as to which
346      * numbering plan to use.
347      * <li><i>mcc</i> The mobile carrier code (MCC) associated with the carrier that the phone is
348      * currently connected to, if known. This also can give a clue as to which numbering plan to
349      * use
350      * <li><i>onLoad</i> - a callback function to call when the date format object is fully
351      * loaded. When the onLoad option is given, the DateFmt object will attempt to
352      * load any missing locale data using the ilib loader callback.
353      * When the constructor is done (even if the data is already preassembled), the
354      * onLoad function is called with the current instance as a parameter, so this
355      * callback can be used with preassembled or dynamic loading or a mix of the two.
356      * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
357      * asynchronously. If this option is given as "false", then the "onLoad"
358      * callback must be given, as the instance returned from this constructor will
359      * not be usable for a while.
360      * <li><i>loadParams</i> - an object containing parameters to pass to the
361      * loader callback function when locale data is missing. The parameters are not
362      * interpretted or modified in any way. They are simply passed along. The object
363      * may contain any property/value pairs as long as the calling code is in
364      * agreement with the loader callback function as to what those parameters mean.
365      * </ul>
366      *
367      * The partial parameter specifies whether or not the phone number contains
368      * a partial phone number or if it is a whole phone number. A partial
369      * number is usually a number as the user is entering it with a dial pad. The
370      * reason is that certain types of phone numbers should be formatted differently
371      * depending on whether or not it represents a whole number. Specifically, SMS
372      * short codes are formatted differently.<p>
373      *
374      * Example: a subscriber number of "48773" in the US would get formatted as:
375      *
376      * <ul>
377      * <li>partial: 487-73  (perhaps the user is in the process of typing a whole phone
378      * number such as 487-7379)
379      * <li>whole:   48773   (this is the entire SMS short code)
380      * </ul>
381      *
382      * Any place in the UI where the user types in phone numbers, such as the keypad in
383      * the phone app, should pass in partial: true to this formatting routine. All other
384      * places, such as the call log in the phone app, should pass in partial: false, or
385      * leave the partial flag out of the parameters entirely.
386      *
387      * @param {!PhoneNumber} number object containing the phone number to format
388      * @param {{
389      *   partial:boolean,
390      *   style:string,
391      *   mcc:string,
392      *   locale:(string|Locale),
393      *   sync:boolean,
394      *   loadParams:Object,
395      *   onLoad:function(string)
396      * }} options Parameters which control how to format the number
397      * @return {string} Returns the formatted phone number as a string.
398      */
399     format: function (number, options) {
400         var formatted = "",
401             callback;
402 
403         callback = options && options.onLoad;
404 
405         try {
406             this._doFormat(number, options, 0, this.locale, this.fmtdata, function (fmt) {
407                 formatted = fmt;
408 
409                 if (typeof(callback) === 'function') {
410                     callback(fmt);
411                 }
412             });
413         } catch (e) {
414             if (typeof(e) === 'string') {
415                 // console.warn("caught exception: " + e + ". Using last resort rule.");
416                 // if there was some exception, use this last resort rule
417                 formatted = "";
418                 for (var field in PhoneNumber._fieldOrder) {
419                     if (typeof field === 'string' && typeof PhoneNumber._fieldOrder[field] === 'string' && number[PhoneNumber._fieldOrder[field]] !== undefined) {
420                         // just concatenate without any formatting
421                         formatted += number[PhoneNumber._fieldOrder[field]];
422                         if (PhoneNumber._fieldOrder[field] === 'countryCode') {
423                             formatted += ' ';        // fix for NOV-107894
424                         }
425                     }
426                 }
427             } else {
428                 throw e;
429             }
430 
431             if (typeof(callback) === 'function') {
432                 callback(formatted);
433             }
434         }
435         return formatted;
436     },
437 
438     /**
439      * Return an array of names of all available styles that can be used with the current
440      * formatter.
441      * @return {Array.<string>} an array of names of styles that are supported by this formatter
442      */
443     getAvailableStyles: function () {
444         var ret = [],
445             style;
446 
447         if (this.fmtdata) {
448             for (style in this.fmtdata) {
449                 if (this.fmtdata[style].example) {
450                     ret.push(style);
451                 }
452             }
453         }
454         return ret;
455     },
456 
457     /**
458      * Return an example phone number formatted with the given style.
459      *
460      * @param {string|undefined} style style to get an example of, or undefined to use
461      * the current default style for this formatter
462      * @return {string|undefined} an example phone number formatted according to the
463      * given style, or undefined if the style is not recognized or does not have an
464      * example
465      */
466     getStyleExample: function (style) {
467         return this.fmtdata[style].example || undefined;
468     }
469 };
470 
471 module.exports = PhoneFmt;