Source

PhoneFmt.js

/*
 * PhoneFmt.js - Represent a phone number formatter.
 *
 * Copyright © 2014-2015, 2018-2019, 2023 JEDLSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// !data phonefmt

var ilib = require("../index.js");
var Utils = require("./Utils.js");
var JSUtils = require("./JSUtils.js");
var Locale = require("./Locale.js");
var PhoneNumber = require("./PhoneNumber.js");
var PhoneLocale = require("./PhoneLocale.js");

/**
 * @class
 * Create a new phone number formatter object that formats numbers according to the parameters.<p>
 *
 * The options object can contain zero or more of the following parameters:
 *
 * <ul>
 * <li><i>locale</i> locale to use to format this number, or undefined to use the default locale
 * <li><i>style</i> the name of style to use to format numbers, or undefined to use the default style
 * <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
 *
 * <li><i>onLoad</i> - a callback function to call when the locale data is fully loaded and the address has been
 * parsed. When the onLoad option is given, the address formatter object
 * will attempt to load any missing locale data using the ilib loader callback.
 * When the constructor is done (even if the data is already preassembled), the
 * onLoad function is called with the current instance as a parameter, so this
 * callback can be used with preassembled or dynamic loading or a mix of the two.
 *
 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
 * asynchronously. If this option is given as "false", then the "onLoad"
 * callback must be given, as the instance returned from this constructor will
 * not be usable for a while.
 *
 * <li><i>loadParams</i> - an object containing parameters to pass to the
 * loader callback function when locale data is missing. The parameters are not
 * interpretted or modified in any way. They are simply passed along. The object
 * may contain any property/value pairs as long as the calling code is in
 * agreement with the loader callback function as to what those parameters mean.
 * </ul>
 *
 * Some regions have more than one style of formatting, and the style parameter
 * selects which style the user prefers. An array of style names that this locale
 * supports can be found by calling {@link PhoneFmt.getAvailableStyles}.
 * Example phone numbers can be retrieved for each style by calling
 * {@link PhoneFmt.getStyleExample}.
 * <p>
 *
 * If the MCC is given, numbers will be formatted in the manner of the country
 * specified by the MCC. If it is not given, but the locale is, the manner of
 * the country in the locale will be used. If neither the locale or MCC are not given,
 * then the country of the current ilib locale is used.
 *
 * @constructor
 * @param {Object} options properties that control how this formatter behaves
 */
var PhoneFmt = function(options) {
    this.sync = true;
    this.styleName = 'default';
    this.loadParams = {};

    var locale = new Locale();

    if (options) {
        if (options.locale) {
            locale = options.locale;
        }

        if (typeof(options.sync) !== 'undefined') {
            this.sync = !!options.sync;
        }

        if (options.loadParams) {
            this.loadParams = options.loadParams;
        }

        if (options.style) {
            this.style = options.style;
        }
    }

    new PhoneLocale({
        locale: locale,
        mcc: options && options.mcc,
        countryCode: options && options.countryCode,
        onLoad: ilib.bind(this, function (data) {
            /** @type {PhoneLocale} */
            this.locale = data;

            Utils.loadData({
                name: "phonefmt.json",
                object: "PhoneFmt",
                locale: this.locale,
                sync: this.sync,
                loadParams: JSUtils.merge(this.loadParams, {
                    returnOne: true
                }),
                callback: ilib.bind(this, function (fmtdata) {
                    this.fmtdata = fmtdata;

                    if (options && typeof(options.onLoad) === 'function') {
                        options.onLoad(this);
                    }
                })
            });
        })
    });
};

PhoneFmt.prototype = {
    /**
     *
     * @private
     * @param {string} part
     * @param {Object} formats
     * @param {boolean} mustUseAll
     */
    _substituteDigits: function(part, formats, mustUseAll) {
        var formatString,
            formatted = "",
            partIndex = 0,
            templates,
            i;

        // console.info("Globalization.Phone._substituteDigits: typeof(formats) is " + typeof(formats));
        if (!part) {
            return formatted;
        }

        if (typeof(formats) === "object") {
            templates = (typeof(formats.template) !== 'undefined') ? formats.template : formats;
            if (part.length > templates.length) {
                // too big, so just use last resort rule.
                throw "part " + part + " is too big. We do not have a format template to format it.";
            }
            // use the format in this array that corresponds to the digit length of this
            // part of the phone number
            formatString =  templates[part.length-1];
            // console.info("Globalization.Phone._substituteDigits: formats is an Array: " + JSON.stringify(formats));
        } else {
            formatString = formats;
        }

        for (i = 0; i < formatString.length; i++) {
            if (formatString.charAt(i) === "X") {
                formatted += part.charAt(partIndex);
                partIndex++;
            } else {
                formatted += formatString.charAt(i);
            }
        }

        if (mustUseAll && partIndex < part.length-1) {
            // didn't use the whole thing in this format? Hmm... go to last resort rule
            throw "too many digits in " + part + " for format " + formatString;
        }

        return formatted;
    },

    /**
     * Returns the style with the given name, or the default style if there
     * is no style with that name.
     * @private
     * @return {{example:string,whole:Object.<string,string>,partial:Object.<string,string>}|Object.<string,string>}
     */
    _getStyle: function (name, fmtdata) {
        return fmtdata[name] || fmtdata["default"];
    },

    /**
     * Do the actual work of formatting the phone number starting at the given
     * field in the regular field order.
     * @private
     * @param {!PhoneNumber} number
     * @param {{
     *   partial:boolean,
     *   style:string,
     *   mcc:string,
     *   locale:(string|Locale),
     *   sync:boolean,
     *   loadParams:Object,
     *   onLoad:function(string)
     * }} options Parameters which control how to format the number
     * @param {number} startField
     */
    _doFormat: function(number, options, startField, locale, fmtdata, callback) {
        var sync = true,
            loadParams = {},
            temp,
            templates,
            fieldName,
            countryCode,
            isWhole,
            style,
            formatted = "",
            styleTemplates,
            lastFieldName;

        if (options) {
            if (typeof(options.sync) !== 'undefined') {
                sync = !!options.sync;
            }

            if (options.loadParams) {
                loadParams = options.loadParams;
            }
        }

        style = this.style; // default style for this formatter

        // figure out what style to use for this type of number
        if (number.countryCode) {
            // dialing from outside the country
            // check to see if it to a mobile number because they are often formatted differently
            style = (number.mobilePrefix) ? "internationalmobile" : "international";
        } else if (number.mobilePrefix !== undefined) {
            style = "mobile";
        } else if (number.serviceCode !== undefined && typeof(fmtdata["service"]) !== 'undefined') {
            // if there is a special format for service numbers, then use it
            style = "service";
        }

        isWhole = (!options || !options.partial);
        styleTemplates = this._getStyle(style, fmtdata);

        // console.log("Style ends up being " + style + " and using subtype " + (isWhole ? "whole" : "partial"));
        styleTemplates = (isWhole ? styleTemplates.whole : styleTemplates.partial) || styleTemplates;

        for (var i = startField; i < PhoneNumber._fieldOrder.length; i++) {
            fieldName = PhoneNumber._fieldOrder[i];
            // console.info("format: formatting field " + fieldName + " value: " + number[fieldName]);
            if (number[fieldName] !== undefined) {
                if (styleTemplates[fieldName] !== undefined) {
                    templates = styleTemplates[fieldName];
                    if (fieldName === "trunkAccess") {
                        if (number.areaCode === undefined && number.serviceCode === undefined && number.mobilePrefix === undefined) {
                            templates = "X";
                        }
                    }
                    if (lastFieldName && typeof(styleTemplates[lastFieldName].suffix) !== 'undefined') {
                        if (fieldName !== "extension" && number[fieldName].search(/[xwtp,;]/i) <= -1) {
                            formatted += styleTemplates[lastFieldName].suffix;
                        }
                    }
                    lastFieldName = fieldName;

                    // console.info("format: formatting field " + fieldName + " with templates " + JSON.stringify(templates));
                    temp = this._substituteDigits(number[fieldName], templates, (fieldName === "subscriberNumber"));
                    // console.info("format: formatted is: " + temp);
                    formatted += temp;

                    if (fieldName === "countryCode") {
                        // switch to the new country to format the rest of the number
                        countryCode = number.countryCode.replace(/[wWpPtT\+#\*]/g, '');    // fix for NOV-108200

                        new PhoneLocale({
                            locale: this.locale,
                            sync: sync,
                            loadParms: loadParams,
                            countryCode: countryCode,
                            onLoad: ilib.bind(this, function (locale) {
                                Utils.loadData({
                                    name: "phonefmt.json",
                                    object: "PhoneFmt",
                                    locale: locale,
                                    sync: sync,
                                    loadParams: JSUtils.merge(loadParams, {
                                        returnOne: true
                                    }),
                                    callback: ilib.bind(this, function (fmtdata) {
                                        // console.info("format: switching to region " + locale.region + " and style " + style + " to format the rest of the number ");

                                        var subfmt = "";

                                        this._doFormat(number, options, i+1, locale, fmtdata, function (subformat) {
                                            subfmt = subformat;
                                            if (typeof(callback) === 'function') {
                                                callback(formatted + subformat);
                                            }
                                        });

                                        formatted += subfmt;
                                    })
                                });
                            })
                        });
                        return formatted;
                    }
                } else {
                    //console.warn("PhoneFmt.format: cannot find format template for field " + fieldName + ", region " + locale.region + ", style " + style);
                    // use default of "minimal formatting" so we don't miss parts because of bugs in the format templates
                    formatted += number[fieldName];
                }
            }
        }

        if (typeof(callback) === 'function') {
            callback(formatted);
        }

        return formatted;
    },

    /**
     * Format the parts of a phone number appropriately according to the settings in
     * this formatter instance.
     *
     * The options can contain zero or more of these properties:
     *
     * <ul>
     * <li><i>partial</i> boolean which tells whether or not this phone number
     * represents a partial number or not. The default is false, which means the number
     * represents a whole number.
     * <li><i>style</i> style to use to format the number, if different from the
     * default style or the style specified in the constructor
     * <li><i>locale</i> The locale with which to parse the number. This gives a clue as to which
     * numbering plan to use.
     * <li><i>mcc</i> The mobile carrier code (MCC) associated with the carrier that the phone is
     * currently connected to, if known. This also can give a clue as to which numbering plan to
     * use
     * <li><i>onLoad</i> - a callback function to call when the date format object is fully
     * loaded. When the onLoad option is given, the DateFmt object will attempt to
     * load any missing locale data using the ilib loader callback.
     * When the constructor is done (even if the data is already preassembled), the
     * onLoad function is called with the current instance as a parameter, so this
     * callback can be used with preassembled or dynamic loading or a mix of the two.
     * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
     * asynchronously. If this option is given as "false", then the "onLoad"
     * callback must be given, as the instance returned from this constructor will
     * not be usable for a while.
     * <li><i>loadParams</i> - an object containing parameters to pass to the
     * loader callback function when locale data is missing. The parameters are not
     * interpretted or modified in any way. They are simply passed along. The object
     * may contain any property/value pairs as long as the calling code is in
     * agreement with the loader callback function as to what those parameters mean.
     * </ul>
     *
     * The partial parameter specifies whether or not the phone number contains
     * a partial phone number or if it is a whole phone number. A partial
     * number is usually a number as the user is entering it with a dial pad. The
     * reason is that certain types of phone numbers should be formatted differently
     * depending on whether or not it represents a whole number. Specifically, SMS
     * short codes are formatted differently.<p>
     *
     * Example: a subscriber number of "48773" in the US would get formatted as:
     *
     * <ul>
     * <li>partial: 487-73  (perhaps the user is in the process of typing a whole phone
     * number such as 487-7379)
     * <li>whole:   48773   (this is the entire SMS short code)
     * </ul>
     *
     * Any place in the UI where the user types in phone numbers, such as the keypad in
     * the phone app, should pass in partial: true to this formatting routine. All other
     * places, such as the call log in the phone app, should pass in partial: false, or
     * leave the partial flag out of the parameters entirely.
     *
     * @param {!PhoneNumber} number object containing the phone number to format
     * @param {{
     *   partial:boolean,
     *   style:string,
     *   mcc:string,
     *   locale:(string|Locale),
     *   sync:boolean,
     *   loadParams:Object,
     *   onLoad:function(string)
     * }} options Parameters which control how to format the number
     * @return {string} Returns the formatted phone number as a string.
     */
    format: function (number, options) {
        var formatted = "",
            callback;

        callback = options && options.onLoad;

        try {
            this._doFormat(number, options, 0, this.locale, this.fmtdata, function (fmt) {
                formatted = fmt;

                if (typeof(callback) === 'function') {
                    callback(fmt);
                }
            });
        } catch (e) {
            if (typeof(e) === 'string') {
                // console.warn("caught exception: " + e + ". Using last resort rule.");
                // if there was some exception, use this last resort rule
                formatted = "";
                for (var field in PhoneNumber._fieldOrder) {
                    if (typeof field === 'string' && typeof PhoneNumber._fieldOrder[field] === 'string' && number[PhoneNumber._fieldOrder[field]] !== undefined) {
                        // just concatenate without any formatting
                        formatted += number[PhoneNumber._fieldOrder[field]];
                        if (PhoneNumber._fieldOrder[field] === 'countryCode') {
                            formatted += ' ';        // fix for NOV-107894
                        }
                    }
                }
            } else {
                throw e;
            }

            if (typeof(callback) === 'function') {
                callback(formatted);
            }
        }
        return formatted;
    },

    /**
     * Return an array of names of all available styles that can be used with the current
     * formatter.
     * @return {Array.<string>} an array of names of styles that are supported by this formatter
     */
    getAvailableStyles: function () {
        var ret = [],
            style;

        if (this.fmtdata) {
            for (style in this.fmtdata) {
                if (this.fmtdata[style].example) {
                    ret.push(style);
                }
            }
        }
        return ret;
    },

    /**
     * Return an example phone number formatted with the given style.
     *
     * @param {string|undefined} style style to get an example of, or undefined to use
     * the current default style for this formatter
     * @return {string|undefined} an example phone number formatted according to the
     * given style, or undefined if the style is not recognized or does not have an
     * example
     */
    getStyleExample: function (style) {
        return this.fmtdata[style].example || undefined;
    }
};

module.exports = PhoneFmt;