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