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;