1 /* 2 * AddressFmt.js - Format an address 3 * 4 * Copyright © 2013-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 address addressres regionnames 21 22 var ilib = require("../index.js"); 23 var Utils = require("./Utils.js"); 24 var JSUtils = require("./JSUtils.js"); 25 26 var Locale = require("./Locale.js"); 27 var Address = require("./Address.js"); 28 var IString = require("./IString.js"); 29 var ResBundle = require("./ResBundle.js"); 30 31 // default generic data 32 var defaultData = { 33 formats: { 34 "default": "{streetAddress}\n{locality} {region} {postalCode}\n{country}", 35 "nocountry": "{streetAddress}\n{locality} {region} {postalCode}" 36 }, 37 startAt: "end", 38 fields: [ 39 { 40 "name": "postalCode", 41 "line": "startAtLast", 42 "pattern": "[0-9]+", 43 "matchGroup": 0 44 }, 45 { 46 "name": "region", 47 "line": "last", 48 "pattern": "([A-zÀÁÈÉÌÍÑÒÓÙÚÜàáèéìíñòóùúü\\.\\-\\']+\\s*){1,2}$", 49 "matchGroup": 0 50 }, 51 { 52 "name": "locality", 53 "line": "last", 54 "pattern": "([A-zÀÁÈÉÌÍÑÒÓÙÚÜàáèéìíñòóùúü\\.\\-\\']+\\s*){1,2}$", 55 "matchGroup": 0 56 } 57 ], 58 fieldNames: { 59 "streetAddress": "Street Address", 60 "locality": "City", 61 "postalCode": "Zip Code", 62 "region": "State", 63 "country": "Country" 64 } 65 }; 66 67 /** 68 * @class 69 * Create a new formatter object to format physical addresses in a particular way. 70 * 71 * The options object may contain the following properties, both of which are optional: 72 * 73 * <ul> 74 * <li><i>locale</i> - the locale to use to format this address. If not specified, it uses the default locale 75 * 76 * <li><i>style</i> - the style of this address. The default style for each country usually includes all valid 77 * fields for that country. 78 * 79 * <li><i>onLoad</i> - a callback function to call when the address info for the 80 * locale is fully loaded and the address has been parsed. When the onLoad 81 * option is given, the address formatter object 82 * will attempt to load any missing locale data using the ilib loader callback. 83 * When the constructor is done (even if the data is already preassembled), the 84 * onLoad function is called with the current instance as a parameter, so this 85 * callback can be used with preassembled or dynamic loading or a mix of the two. 86 * 87 * <li><i>sync</i> - tell whether to load any missing locale data synchronously or 88 * asynchronously. If this option is given as "false", then the "onLoad" 89 * callback must be given, as the instance returned from this constructor will 90 * not be usable for a while. 91 * 92 * <li><i>loadParams</i> - an object containing parameters to pass to the 93 * loader callback function when locale data is missing. The parameters are not 94 * interpretted or modified in any way. They are simply passed along. The object 95 * may contain any property/value pairs as long as the calling code is in 96 * agreement with the loader callback function as to what those parameters mean. 97 * </ul> 98 * 99 * 100 * @constructor 101 * @param {Object} options options that configure how this formatter should work 102 * Returns a formatter instance that can format multiple addresses. 103 */ 104 var AddressFmt = function(options) { 105 this.sync = true; 106 this.styleName = 'default'; 107 this.loadParams = {}; 108 this.locale = new Locale(); 109 110 if (options) { 111 if (options.locale) { 112 this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale; 113 } 114 115 if (typeof(options.sync) !== 'undefined') { 116 this.sync = !!options.sync; 117 } 118 119 if (options.style) { 120 this.styleName = options.style; 121 } 122 123 if (options.loadParams) { 124 this.loadParams = options.loadParams; 125 } 126 } 127 128 // console.log("Creating formatter for region: " + this.locale.region); 129 Utils.loadData({ 130 name: "address.json", 131 object: "AddressFmt", 132 locale: this.locale, 133 sync: this.sync, 134 loadParams: this.loadParams, 135 callback: ilib.bind(this, function(info) { 136 if (!info || JSUtils.isEmpty(info)) { 137 // load the "unknown" locale instead 138 Utils.loadData({ 139 name: "address.json", 140 object: "AddressFmt", 141 locale: new Locale("XX"), 142 sync: this.sync, 143 loadParams: this.loadParams, 144 callback: ilib.bind(this, function(info) { 145 this.info = info; 146 this._init(); 147 if (options && typeof(options.onLoad) === 'function') { 148 options.onLoad(this); 149 } 150 }) 151 }); 152 } else { 153 this.info = info; 154 this._init(); 155 if (options && typeof(options.onLoad) === 'function') { 156 options.onLoad(this); 157 } 158 } 159 }) 160 }); 161 }; 162 163 /** 164 * @private 165 */ 166 AddressFmt.prototype._init = function () { 167 if (!this.info) this.info = defaultData; 168 169 this.style = this.info.formats && this.info.formats[this.styleName]; 170 171 // use generic default -- should not happen, but just in case... 172 this.style = this.style || (this.info.formats && this.info.formats["default"]) || defaultData.formats["default"]; 173 174 if (!this.info.fieldNames) { 175 this.info.fieldNames = defaultData.fieldNames; 176 } 177 }; 178 179 /** 180 * This function formats a physical address (Address instance) for display. 181 * Whitespace is trimmed from the beginning and end of final resulting string, and 182 * multiple consecutive whitespace characters in the middle of the string are 183 * compressed down to 1 space character. 184 * 185 * If the Address instance is for a locale that is different than the locale for this 186 * formatter, then a hybrid address is produced. The country name is located in the 187 * correct spot for the current formatter's locale, but the rest of the fields are 188 * formatted according to the default style of the locale of the actual address. 189 * 190 * Example: a mailing address in China, but formatted for the US might produce the words 191 * "People's Republic of China" in English at the last line of the address, and the 192 * Chinese-style address will appear in the first line of the address. In the US, the 193 * country is on the last line, but in China the country is usually on the first line. 194 * 195 * @param {Address} address Address to format 196 * @returns {string} Returns a string containing the formatted address 197 */ 198 AddressFmt.prototype.format = function (address) { 199 var ret, template, other, format; 200 201 if (!address) { 202 return ""; 203 } 204 // console.log("formatting address: " + JSON.stringify(address)); 205 if (address.countryCode && 206 address.countryCode !== this.locale.region && 207 Locale._isRegionCode(this.locale.region) && 208 this.locale.region !== "XX") { 209 // we are formatting an address that is sent from this country to another country, 210 // so only the country should be in this locale, and the rest should be in the other 211 // locale 212 // console.log("formatting for another locale. Loading in its settings: " + address.countryCode); 213 other = new AddressFmt({ 214 locale: new Locale(address.countryCode), 215 style: this.styleName 216 }); 217 return other.format(address); 218 } 219 220 if (typeof(this.style) === 'object') { 221 format = this.style[address.format || "latin"]; 222 } else { 223 format = this.style; 224 } 225 226 // console.log("Using format: " + format); 227 // make sure we have a blank string for any missing parts so that 228 // those template parts get blanked out 229 var params = { 230 country: address.country || "", 231 region: address.region || "", 232 locality: address.locality || "", 233 streetAddress: address.streetAddress || "", 234 postalCode: address.postalCode || "", 235 postOffice: address.postOffice || "" 236 }; 237 template = new IString(format); 238 ret = template.format(params); 239 ret = ret.replace(/[ \t]+/g, ' '); 240 ret = ret.replace("\n ", "\n"); 241 ret = ret.replace(" \n", "\n"); 242 return ret.replace(/\n+/g, '\n').trim(); 243 }; 244 245 246 /** 247 * Return true if this is an asian locale. 248 * @private 249 * @returns {boolean} true if this is an asian locale, or false otherwise 250 */ 251 function isAsianLocale(locale) { 252 return locale.language === "zh" || locale.language === "ja" || locale.language === "ko"; 253 } 254 255 /** 256 * Invert the properties and values, filtering out all the regions. Regions either 257 * have values with numbers (eg. "150" for Europe), or they are on a short list of 258 * known regions with actual ISO codes. 259 * 260 * @private 261 * @returns {Object} the inverted object 262 */ 263 function invertAndFilter(object) { 264 var ret = []; 265 var regions = ["AQ", "EU", "EZ", "UN", "ZZ"] 266 for (var p in object) { 267 if (p && !object[p].match(/\d/) && regions.indexOf(object[p]) === -1) { 268 ret.push({ 269 code: object[p], 270 name: p 271 }); 272 } 273 } 274 275 return ret; 276 } 277 278 /** 279 * Return information about the address format that can be used 280 * by UI builders to display a locale-sensitive set of input fields 281 * based on the current formatter's settings.<p> 282 * 283 * The object returned by this method is an array of address rows. Each 284 * row is itself an array which may have one to four address 285 * components in that row. Each address component is an object 286 * that contains a component property and a label to display 287 * with it. The label is written in the given locale, or the 288 * locale of this formatter if it was not given.<p> 289 * 290 * Optionally, if the address component is constrained to a 291 * particular pattern or to a fixed list of possible values, then 292 * the constraint rules are given in the "constraint" property.<p> 293 * 294 * If an address component must conform to a particular pattern, 295 * the regular expression that matches that pattern 296 * is returned in "constraint". Mostly, it is only the postal code 297 * component that can be validated in this way.<p> 298 * 299 * If an address component should be limited 300 * to a fixed list of values, then the constraint property will be 301 * set to an array that lists those values. The constraint contains 302 * an array of objects in the correct sorted order for the locale 303 * where each object contains an code property containing the ISO code, 304 * and a name field to show in UI. 305 * The ISO codes should not be shown to the user and are intended to 306 * represent the values in code. The names are translated to the given 307 * locale or to the locale of this formatter if it was not given. For 308 * the most part, it is the region and country components that 309 * are constrained in this way.<p> 310 * 311 * Here is what the result would look like for a US address: 312 * <pre> 313 * [ 314 * [{ 315 * "component": "streetAddress", 316 * "label": "Street Address" 317 * }], 318 * [{ 319 * "component": "locality", 320 * "label": "City" 321 * },{ 322 * "component": "region", 323 * "label": "State", 324 * "constraint": [{ 325 * "code": "AL", 326 * "name": "Alabama" 327 * },{ 328 * "code": "AK", 329 * "name": "Alaska" 330 * },{ 331 * ... 332 * },{ 333 * "code": "WY", 334 * "name": "Wyoming" 335 * } 336 * },{ 337 * "component": "postalCode", 338 * "label": "Zip Code", 339 * "constraint": "[0-9]{5}(-[0-9]{4})?" 340 * }], 341 * [{ 342 * "component": "country", 343 * "label": "Country", 344 * "constraint": [{ 345 * "code": "AF", 346 * "name": "Afghanistan" 347 * },{ 348 * "code": "AL", 349 * "name": "Albania" 350 * },{ 351 * ... 352 * },{ 353 * "code": "ZW", 354 * "name": "Zimbabwe" 355 * }] 356 * }] 357 * ] 358 * </pre> 359 * <p> 360 * @example <caption>Example of calling the getFormatInfo method</caption> 361 * 362 * // the AddressFmt should be created with the locale of the address you 363 * // would like the user to enter. For example, if you have a "country" 364 * // selector, you would create a new AddressFmt instance each time the 365 * // selector is changed. 366 * new AddressFmt({ 367 * locale: 'nl-NL', // for addresses in the Netherlands 368 * onLoad: ilib.bind(this, function(fmt) { 369 * // The following is the locale of the UI you would like to see the labels 370 * // like "City" and "Postal Code" translated to. In this example, we 371 * // are showing an input form for Dutch addresses, but the labels are 372 * // written in US English. 373 * fmt.getAddressFormatInfo("en-US", true, ilib.bind(this, function(rows) { 374 * // iterate through the rows array and dynamically create the input 375 * // elements with the given labels 376 * })); 377 * }) 378 * }); 379 * 380 * @param {Locale|string=} locale the locale to translate the labels 381 * to. If not given, the locale of the formatter will be used. 382 * @param {boolean=} sync true if this method should load the data 383 * synchronously, false if async 384 * @param {Function=} callback a callback to call when the data 385 * is ready 386 * @returns {Array.<Object>} An array of rows of address components 387 */ 388 AddressFmt.prototype.getFormatInfo = function(locale, sync, callback) { 389 var info; 390 var loc = new Locale(this.locale); 391 if (locale) { 392 if (typeof(locale) === "string") { 393 locale = new Locale(locale); 394 } 395 loc.language = locale.getLanguage(); 396 loc.spec = undefined; 397 } 398 399 Utils.loadData({ 400 name: "regionnames.json", 401 object: "AddressFmt", 402 locale: loc, 403 sync: this.sync, 404 loadParams: JSUtils.merge(this.loadParams, {returnOne: true}, true), 405 callback: ilib.bind(this, function(regions) { 406 this.regions = regions; 407 408 new ResBundle({ 409 locale: loc, 410 name: "addressres", 411 sync: this.sync, 412 loadParams: this.loadParams, 413 onLoad: ilib.bind(this, function (rb) { 414 var type, format, fields = this.info.fields || defaultData.fields; 415 if (this.info.multiformat) { 416 type = isAsianLocale(this.locale) ? "asian" : "latin"; 417 fields = this.info.fields[type]; 418 } 419 420 if (typeof(this.style) === 'object') { 421 format = this.style[type || "latin"]; 422 } else { 423 format = this.style; 424 } 425 new Address(" ", { 426 locale: loc, 427 sync: this.sync, 428 loadParams: this.loadParams, 429 onLoad: ilib.bind(this, function(localeAddress) { 430 var rows = format.split(/\n/g); 431 info = rows.map(ilib.bind(this, function(row) { 432 return row.split("}").filter(function(component) { 433 return component.length > 0; 434 }).map(ilib.bind(this, function(component) { 435 var name = component.replace(/.*{/, ""); 436 var obj = { 437 component: name, 438 label: rb.getStringJS(this.info.fieldNames[name]) 439 }; 440 var field = fields.filter(function(f) { 441 return f.name === name; 442 }); 443 if (field && field[0] && field[0].pattern) { 444 if (typeof(field[0].pattern) === "string") { 445 obj.constraint = field[0].pattern; 446 } 447 } 448 if (name === "country") { 449 obj.constraint = invertAndFilter(localeAddress.ctrynames); 450 } else if (name === "region" && this.regions[loc.getRegion()]) { 451 obj.constraint = this.regions[loc.getRegion()]; 452 } 453 return obj; 454 })); 455 })); 456 457 if (callback && typeof(callback) === "function") { 458 callback(info); 459 } 460 }) 461 }); 462 }) 463 }); 464 }) 465 }); 466 467 return info; 468 }; 469 470 module.exports = AddressFmt; 471