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