/*
* AddressFmt.js - Format an address
*
* Copyright © 2013-2015, 2018, 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 address addressres regionnames
var ilib = require("../index.js");
var Utils = require("./Utils.js");
var JSUtils = require("./JSUtils.js");
var Locale = require("./Locale.js");
var Address = require("./Address.js");
var IString = require("./IString.js");
var ResBundle = require("./ResBundle.js");
// default generic data
var defaultData = {
formats: {
"default": "{streetAddress}\n{locality} {region} {postalCode}\n{country}",
"nocountry": "{streetAddress}\n{locality} {region} {postalCode}"
},
startAt: "end",
fields: [
{
"name": "postalCode",
"line": "startAtLast",
"pattern": "[0-9]+",
"matchGroup": 0
},
{
"name": "region",
"line": "last",
"pattern": "([A-zÀÁÈÉÌÍÑÒÓÙÚÜàáèéìíñòóùúü\\.\\-\\']+\\s*){1,2}$",
"matchGroup": 0
},
{
"name": "locality",
"line": "last",
"pattern": "([A-zÀÁÈÉÌÍÑÒÓÙÚÜàáèéìíñòóùúü\\.\\-\\']+\\s*){1,2}$",
"matchGroup": 0
}
],
fieldNames: {
"streetAddress": "Street Address",
"locality": "City",
"postalCode": "Zip Code",
"region": "State",
"country": "Country"
}
};
/**
* @class
* Create a new formatter object to format physical addresses in a particular way.
*
* The options object may contain the following properties, both of which are optional:
*
* <ul>
* <li><i>locale</i> - the locale to use to format this address. If not specified, it uses the default locale
*
* <li><i>style</i> - the style of this address. The default style for each country usually includes all valid
* fields for that country.
*
* <li><i>onLoad</i> - a callback function to call when the address info for the
* locale 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>
*
*
* @constructor
* @param {Object} options options that configure how this formatter should work
* Returns a formatter instance that can format multiple addresses.
*/
var AddressFmt = function(options) {
this.sync = true;
this.styleName = 'default';
this.loadParams = {};
this.locale = new Locale();
if (options) {
if (options.locale) {
this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
}
if (typeof(options.sync) !== 'undefined') {
this.sync = !!options.sync;
}
if (options.style) {
this.styleName = options.style;
}
if (options.loadParams) {
this.loadParams = options.loadParams;
}
}
// console.log("Creating formatter for region: " + this.locale.region);
Utils.loadData({
name: "address.json",
object: "AddressFmt",
locale: this.locale,
sync: this.sync,
loadParams: this.loadParams,
callback: ilib.bind(this, function(info) {
if (!info || JSUtils.isEmpty(info)) {
// load the "unknown" locale instead
Utils.loadData({
name: "address.json",
object: "AddressFmt",
locale: new Locale("XX"),
sync: this.sync,
loadParams: this.loadParams,
callback: ilib.bind(this, function(info) {
this.info = info;
this._init();
if (options && typeof(options.onLoad) === 'function') {
options.onLoad(this);
}
})
});
} else {
this.info = info;
this._init();
if (options && typeof(options.onLoad) === 'function') {
options.onLoad(this);
}
}
})
});
};
/**
* @private
*/
AddressFmt.prototype._init = function () {
if (!this.info) this.info = defaultData;
this.style = this.info.formats && this.info.formats[this.styleName];
// use generic default -- should not happen, but just in case...
this.style = this.style || (this.info.formats && this.info.formats["default"]) || defaultData.formats["default"];
if (!this.info.fieldNames) {
this.info.fieldNames = defaultData.fieldNames;
}
};
/**
* This function formats a physical address (Address instance) for display.
* Whitespace is trimmed from the beginning and end of final resulting string, and
* multiple consecutive whitespace characters in the middle of the string are
* compressed down to 1 space character.
*
* If the Address instance is for a locale that is different than the locale for this
* formatter, then a hybrid address is produced. The country name is located in the
* correct spot for the current formatter's locale, but the rest of the fields are
* formatted according to the default style of the locale of the actual address.
*
* Example: a mailing address in China, but formatted for the US might produce the words
* "People's Republic of China" in English at the last line of the address, and the
* Chinese-style address will appear in the first line of the address. In the US, the
* country is on the last line, but in China the country is usually on the first line.
*
* @param {Address} address Address to format
* @returns {string} Returns a string containing the formatted address
*/
AddressFmt.prototype.format = function (address) {
var ret, template, other, format;
if (!address) {
return "";
}
// console.log("formatting address: " + JSON.stringify(address));
if (address.countryCode &&
address.countryCode !== this.locale.region &&
Locale._isRegionCode(this.locale.region) &&
this.locale.region !== "XX") {
// we are formatting an address that is sent from this country to another country,
// so only the country should be in this locale, and the rest should be in the other
// locale
// console.log("formatting for another locale. Loading in its settings: " + address.countryCode);
other = new AddressFmt({
locale: new Locale(address.countryCode),
style: this.styleName
});
return other.format(address);
}
if (typeof(this.style) === 'object') {
format = this.style[address.format || "latin"];
} else {
format = this.style;
}
// console.log("Using format: " + format);
// make sure we have a blank string for any missing parts so that
// those template parts get blanked out
var params = {
country: address.country || "",
region: address.region || "",
locality: address.locality || "",
streetAddress: address.streetAddress || "",
postalCode: address.postalCode || "",
postOffice: address.postOffice || ""
};
template = new IString(format);
ret = template.format(params);
ret = ret.replace(/[ \t]+/g, ' ');
ret = ret.replace("\n ", "\n");
ret = ret.replace(" \n", "\n");
return ret.replace(/\n+/g, '\n').trim();
};
/**
* Return true if this is an asian locale.
* @private
* @returns {boolean} true if this is an asian locale, or false otherwise
*/
function isAsianLocale(locale) {
return locale.language === "zh" || locale.language === "ja" || locale.language === "ko";
}
/**
* Invert the properties and values, filtering out all the regions. Regions either
* have values with numbers (eg. "150" for Europe), or they are on a short list of
* known regions with actual ISO codes.
*
* @private
* @returns {Object} the inverted object
*/
function invertAndFilter(object) {
var ret = [];
var regions = ["AQ", "EU", "EZ", "UN", "ZZ"]
for (var p in object) {
if (p && !object[p].match(/\d/) && regions.indexOf(object[p]) === -1) {
ret.push({
code: object[p],
name: p
});
}
}
return ret;
}
/**
* Return information about the address format that can be used
* by UI builders to display a locale-sensitive set of input fields
* based on the current formatter's settings.<p>
*
* The object returned by this method is an array of address rows. Each
* row is itself an array which may have one to four address
* components in that row. Each address component is an object
* that contains a component property and a label to display
* with it. The label is written in the given locale, or the
* locale of this formatter if it was not given.<p>
*
* Optionally, if the address component is constrained to a
* particular pattern or to a fixed list of possible values, then
* the constraint rules are given in the "constraint" property.<p>
*
* If an address component must conform to a particular pattern,
* the regular expression that matches that pattern
* is returned in "constraint". Mostly, it is only the postal code
* component that can be validated in this way.<p>
*
* If an address component should be limited
* to a fixed list of values, then the constraint property will be
* set to an array that lists those values. The constraint contains
* an array of objects in the correct sorted order for the locale
* where each object contains an code property containing the ISO code,
* and a name field to show in UI.
* The ISO codes should not be shown to the user and are intended to
* represent the values in code. The names are translated to the given
* locale or to the locale of this formatter if it was not given. For
* the most part, it is the region and country components that
* are constrained in this way.<p>
*
* Here is what the result would look like for a US address:
* <pre>
* [
* [{
* "component": "streetAddress",
* "label": "Street Address"
* }],
* [{
* "component": "locality",
* "label": "City"
* },{
* "component": "region",
* "label": "State",
* "constraint": [{
* "code": "AL",
* "name": "Alabama"
* },{
* "code": "AK",
* "name": "Alaska"
* },{
* ...
* },{
* "code": "WY",
* "name": "Wyoming"
* }
* },{
* "component": "postalCode",
* "label": "Zip Code",
* "constraint": "[0-9]{5}(-[0-9]{4})?"
* }],
* [{
* "component": "country",
* "label": "Country",
* "constraint": [{
* "code": "AF",
* "name": "Afghanistan"
* },{
* "code": "AL",
* "name": "Albania"
* },{
* ...
* },{
* "code": "ZW",
* "name": "Zimbabwe"
* }]
* }]
* ]
* </pre>
* <p>
* @example <caption>Example of calling the getFormatInfo method</caption>
*
* // the AddressFmt should be created with the locale of the address you
* // would like the user to enter. For example, if you have a "country"
* // selector, you would create a new AddressFmt instance each time the
* // selector is changed.
* new AddressFmt({
* locale: 'nl-NL', // for addresses in the Netherlands
* onLoad: ilib.bind(this, function(fmt) {
* // The following is the locale of the UI you would like to see the labels
* // like "City" and "Postal Code" translated to. In this example, we
* // are showing an input form for Dutch addresses, but the labels are
* // written in US English.
* fmt.getAddressFormatInfo("en-US", true, ilib.bind(this, function(rows) {
* // iterate through the rows array and dynamically create the input
* // elements with the given labels
* }));
* })
* });
*
* @param {Locale|string=} locale the locale to translate the labels
* to. If not given, the locale of the formatter will be used.
* @param {boolean=} sync true if this method should load the data
* synchronously, false if async
* @param {Function=} callback a callback to call when the data
* is ready
* @returns {Array.<Object>} An array of rows of address components
*/
AddressFmt.prototype.getFormatInfo = function(locale, sync, callback) {
var info;
var loc = new Locale(this.locale);
if (locale) {
if (typeof(locale) === "string") {
locale = new Locale(locale);
}
loc.language = locale.getLanguage();
loc.spec = undefined;
}
Utils.loadData({
name: "regionnames.json",
object: "AddressFmt",
locale: loc,
sync: this.sync,
loadParams: JSUtils.merge(this.loadParams, {returnOne: true}, true),
callback: ilib.bind(this, function(regions) {
this.regions = regions;
new ResBundle({
locale: loc,
name: "addressres",
sync: this.sync,
loadParams: this.loadParams,
onLoad: ilib.bind(this, function (rb) {
var type, format, fields = this.info.fields || defaultData.fields;
if (this.info.multiformat) {
type = isAsianLocale(this.locale) ? "asian" : "latin";
fields = this.info.fields[type];
}
if (typeof(this.style) === 'object') {
format = this.style[type || "latin"];
} else {
format = this.style;
}
new Address(" ", {
locale: loc,
sync: this.sync,
loadParams: this.loadParams,
onLoad: ilib.bind(this, function(localeAddress) {
var rows = format.split(/\n/g);
info = rows.map(ilib.bind(this, function(row) {
return row.split("}").filter(function(component) {
return component.length > 0;
}).map(ilib.bind(this, function(component) {
var name = component.replace(/.*{/, "");
var obj = {
component: name,
label: rb.getStringJS(this.info.fieldNames[name])
};
var field = fields.filter(function(f) {
return f.name === name;
});
if (field && field[0] && field[0].pattern) {
if (typeof(field[0].pattern) === "string") {
obj.constraint = field[0].pattern;
}
}
if (name === "country") {
obj.constraint = invertAndFilter(localeAddress.ctrynames);
} else if (name === "region" && this.regions[loc.getRegion()]) {
obj.constraint = this.regions[loc.getRegion()];
}
return obj;
}));
}));
if (callback && typeof(callback) === "function") {
callback(info);
}
})
});
})
});
})
});
return info;
};
module.exports = AddressFmt;
Source