1 /* 2 * NameFmt.js - Format person names for display 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 name 21 22 var ilib = require("./ilib.js"); 23 var Utils = require("./Utils.js"); 24 25 var Locale = require("./Locale.js"); 26 27 var IString = require("./IString.js"); 28 var Name = require("./Name.js"); 29 var isPunct = require("./isPunct.js"); 30 31 /** 32 * @class 33 * Creates a formatter that can format person name instances (Name) for display to 34 * a user. The options may contain the following properties: 35 * 36 * <ul> 37 * <li><i>locale</i> - Use the conventions of the given locale to construct the name format. 38 * <li><i>style</i> - Format the name with the given style. The value of this property 39 * should be one of the following strings: 40 * <ul> 41 * <li><i>short</i> - Format a short name with just the given and family names. eg. "John Smith" 42 * <li><i>medium</i> - Format a medium-length name with the given, middle, and family names. 43 * eg. "John James Smith" 44 * <li><i>long</i> - Format a long name with all names available in the given name object, including 45 * prefixes. eg. "Mr. John James Smith" 46 * <li><i>full</i> - Format a long name with all names available in the given name object, including 47 * prefixes and suffixes. eg. "Mr. John James Smith, Jr." 48 * <li><i>formal_short</i> - Format a name with the honorific or prefix/suffix and the family 49 * name. eg. "Mr. Smith" 50 * <li><i>formal_long</i> - Format a name with the honorific or prefix/suffix and the 51 * given and family name. eg. "Mr. John Smith" 52 * </ul> 53 * <li><i>components</i> - Format the name with the given components in the correct 54 * order for those components. Components are encoded as a string of letters representing 55 * the desired components: 56 * <ul> 57 * <li><i>p</i> - prefixes 58 * <li><i>g</i> - given name 59 * <li><i>m</i> - middle names 60 * <li><i>f</i> - family name 61 * <li><i>s</i> - suffixes 62 * <li><i>h</i> - honorifics (selects the prefix or suffix as required by the locale) 63 * </ul> 64 * <p> 65 * 66 * For example, the string "pf" would mean to only format any prefixes and family names 67 * together and leave out all the other parts of the name.<p> 68 * 69 * The components can be listed in any order in the string. The <i>components</i> option 70 * overrides the <i>style</i> option if both are specified. 71 * 72 * <li>onLoad - a callback function to call when the locale info object is fully 73 * loaded. When the onLoad option is given, the localeinfo object will attempt to 74 * load any missing locale data using the ilib loader callback. 75 * When the constructor is done (even if the data is already preassembled), the 76 * onLoad function is called with the current instance as a parameter, so this 77 * callback can be used with preassembled or dynamic loading or a mix of the two. 78 * 79 * <li>sync - tell whether to load any missing locale data synchronously or 80 * asynchronously. If this option is given as "false", then the "onLoad" 81 * callback must be given, as the instance returned from this constructor will 82 * not be usable for a while. 83 * 84 * <li><i>loadParams</i> - an object containing parameters to pass to the 85 * loader callback function when locale data is missing. The parameters are not 86 * interpretted or modified in any way. They are simply passed along. The object 87 * may contain any property/value pairs as long as the calling code is in 88 * agreement with the loader callback function as to what those parameters mean. 89 * </ul> 90 * 91 * Formatting names is a locale-dependent function, as the order of the components 92 * depends on the locale. The following explains some of the details:<p> 93 * 94 * <ul> 95 * <li>In Western countries, the given name comes first, followed by a space, followed 96 * by the family name. In Asian countries, the family name comes first, followed immediately 97 * by the given name with no space. But, that format is only used with Asian names written 98 * in ideographic characters. In Asian countries, especially ones where both an Asian and 99 * a Western language are used (Hong Kong, Singapore, etc.), the convention is often to 100 * follow the language of the name. That is, Asian names are written in Asian style, and 101 * Western names are written in Western style. This class follows that convention as 102 * well. 103 * <li>In other Asian countries, Asian names 104 * written in Latin script are written with Asian ordering. eg. "Xu Ping-an" instead 105 * of the more Western order "Ping-an Xu", as the order is thought to go with the style 106 * that is appropriate for the name rather than the style for the language being written. 107 * <li>In some Spanish speaking countries, people often take both their maternal and 108 * paternal last names as their own family name. When formatting a short or medium style 109 * of that family name, only the paternal name is used. In the long style, all the names 110 * are used. eg. "Juan Julio Raul Lopez Ortiz" took the name "Lopez" from his father and 111 * the name "Ortiz" from his mother. His family name would be "Lopez Ortiz". The formatted 112 * short style of his name would be simply "Juan Lopez" which only uses his paternal 113 * family name of "Lopez". 114 * <li>In many Western languages, it is common to use auxillary words in family names. For 115 * example, the family name of "Ludwig von Beethoven" in German is "von Beethoven", not 116 * "Beethoven". This class ensures that the family name is formatted correctly with 117 * all auxillary words. 118 * </ul> 119 * 120 * 121 * @constructor 122 * @param {Object} options A set of options that govern how the formatter will behave 123 */ 124 var NameFmt = function(options) { 125 var sync = true; 126 127 this.style = "short"; 128 this.loadParams = {}; 129 130 if (options) { 131 if (options.locale) { 132 this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale; 133 } 134 135 if (options.style) { 136 this.style = options.style; 137 } 138 139 if (options.components) { 140 this.components = options.components; 141 } 142 143 if (typeof(options.sync) !== 'undefined') { 144 sync = !!options.sync; 145 } 146 147 if (typeof(options.loadParams) !== 'undefined') { 148 this.loadParams = options.loadParams; 149 } 150 } 151 152 // set up defaults in case we need them 153 this.defaultEuroTemplate = new IString("{prefix} {givenName} {middleName} {familyName}{suffix}"); 154 this.defaultAsianTemplate = new IString("{prefix}{familyName}{givenName}{middleName}{suffix}"); 155 this.useFirstFamilyName = false; 156 157 switch (this.style) { 158 default: 159 case "s": 160 case "short": 161 this.style = "short"; 162 break; 163 case "m": 164 case "medium": 165 this.style = "medium"; 166 break; 167 case "l": 168 case "long": 169 this.style = "long"; 170 break; 171 case "f": 172 case "full": 173 this.style = "full"; 174 break; 175 case "fs": 176 case "formal_short": 177 this.style = "formal_short"; 178 break; 179 case "fl": 180 case "formal_long": 181 this.style = "formal_long"; 182 break; 183 } 184 185 this.locale = this.locale || new Locale(); 186 187 isPunct._init(sync, this.loadParams, ilib.bind(this, function() { 188 Utils.loadData({ 189 object: "Name", 190 locale: this.locale, 191 name: "name.json", 192 sync: sync, 193 loadParams: this.loadParams, 194 callback: ilib.bind(this, function (info) { 195 this.info = info || Name.defaultInfo;; 196 this._init(); 197 if (options && typeof(options.onLoad) === 'function') { 198 options.onLoad(this); 199 } 200 }) 201 }); 202 })); 203 }; 204 205 NameFmt.prototype = { 206 /** 207 * @protected 208 */ 209 _init: function() { 210 var arr; 211 this.comps = {}; 212 213 if (this.components) { 214 var valids = {"p":1,"g":1,"m":1,"f":1,"s":1,"h":1}; 215 arr = this.components.split(""); 216 this.comps = {}; 217 for (var i = 0; i < arr.length; i++) { 218 if (valids[arr[i].toLowerCase()]) { 219 this.comps[arr[i].toLowerCase()] = true; 220 } 221 } 222 } else { 223 var comps = this.info.components[this.style]; 224 if (typeof(comps) === "string") { 225 comps.split("").forEach(ilib.bind(this, function(c) { 226 this.comps[c] = true; 227 })); 228 } else { 229 this.comps = comps; 230 } 231 } 232 233 this.template = new IString(this.info.format); 234 235 if (this.locale.language === "es" && (this.style !== "long" && this.style !== "full")) { 236 this.useFirstFamilyName = true; // in spanish, they have 2 family names, the maternal and paternal 237 } 238 239 this.isAsianLocale = (this.info.nameStyle === "asian"); 240 }, 241 242 /** 243 * adjoin auxillary words to their head words 244 * @protected 245 */ 246 _adjoinAuxillaries: function (parts, namePrefix) { 247 var start, i, prefixArray, prefix, prefixLower; 248 249 //console.info("_adjoinAuxillaries: finding and adjoining aux words in " + parts.join(' ')); 250 251 if ( this.info.auxillaries && (parts.length > 2 || namePrefix) ) { 252 for ( start = 0; start < parts.length-1; start++ ) { 253 for ( i = parts.length; i > start; i-- ) { 254 prefixArray = parts.slice(start, i); 255 prefix = prefixArray.join(' '); 256 prefixLower = prefix.toLowerCase(); 257 prefixLower = prefixLower.replace(/[,\.]/g, ''); // ignore commas and periods 258 259 //console.info("_adjoinAuxillaries: checking aux prefix: '" + prefixLower + "' which is " + start + " to " + i); 260 261 if ( prefixLower in this.info.auxillaries ) { 262 //console.info("Found! Old parts list is " + JSON.stringify(parts)); 263 parts.splice(start, i+1-start, prefixArray.concat(parts[i])); 264 //console.info("_adjoinAuxillaries: Found! New parts list is " + JSON.stringify(parts)); 265 i = start; 266 } 267 } 268 } 269 } 270 271 //console.info("_adjoinAuxillaries: done. Result is " + JSON.stringify(parts)); 272 273 return parts; 274 }, 275 276 /** 277 * Return the locale for this formatter instance. 278 * @return {Locale} the locale instance for this formatter 279 */ 280 getLocale: function () { 281 return this.locale; 282 }, 283 284 /** 285 * Return the style of names returned by this formatter 286 * @return {string} the style of names returned by this formatter 287 */ 288 getStyle: function () { 289 return this.style; 290 }, 291 292 /** 293 * Return the list of components used to format names in this formatter 294 * @return {string} the list of components 295 */ 296 getComponents: function () { 297 return this.components; 298 }, 299 300 /** 301 * Format the name for display in the current locale with the options set up 302 * in the constructor of this formatter instance.<p> 303 * 304 * If the name does not contain all the parts required for the style, those parts 305 * will be left blank.<p> 306 * 307 * There are two basic styles of formatting: European, and Asian. If this formatter object 308 * is set for European style, but an Asian name is passed to the format method, then this 309 * method will format the Asian name with a generic Asian template. Similarly, if the 310 * formatter is set for an Asian style, and a European name is passed to the format method, 311 * the formatter will use a generic European template.<p> 312 * 313 * This means it is always safe to format any name with a formatter for any locale. You should 314 * always get something at least reasonable as output.<p> 315 * 316 * @param {Name|Object} name the name instance to format, or an object containing name parts to format 317 * @return {string|undefined} the name formatted according to the style of this formatter instance 318 */ 319 format: function(name) { 320 var formatted, temp, modified, isAsianName; 321 var currentLanguage = this.locale.getLanguage(); 322 323 if (!name || typeof(name) !== 'object') { 324 return undefined; 325 } 326 if (!(name instanceof Name)) { 327 // if the object is not a name, implicitly convert to a name so that the code below works 328 name = new Name(name, {locale: this.locale}); 329 } 330 331 if ((typeof(name.isAsianName) === 'boolean' && !name.isAsianName) || 332 Name._isEuroName([name.givenName, name.middleName, name.familyName].join(""), currentLanguage)) { 333 isAsianName = false; // this is a euro name, even if the locale is asian 334 modified = name.clone(); 335 336 // handle the case where there is no space if there is punctuation in the suffix like ", Phd". 337 // Otherwise, put a space in to transform "PhD" to " PhD" 338 /* 339 console.log("suffix is " + modified.suffix); 340 if ( modified.suffix ) { 341 console.log("first char is " + modified.suffix.charAt(0)); 342 console.log("isPunct(modified.suffix.charAt(0)) is " + isPunct(modified.suffix.charAt(0))); 343 } 344 */ 345 if (modified.suffix && isPunct(modified.suffix.charAt(0)) === false) { 346 modified.suffix = ' ' + modified.suffix; 347 } 348 349 if (this.useFirstFamilyName && name.familyName) { 350 var familyNameParts = modified.familyName.trim().split(' '); 351 if (familyNameParts.length > 1) { 352 familyNameParts = this._adjoinAuxillaries(familyNameParts, name.prefix); 353 } //in spain and mexico, we parse names differently than in the rest of the world 354 355 modified.familyName = familyNameParts[0]; 356 } 357 358 modified._joinNameArrays(); 359 } else { 360 isAsianName = true; 361 modified = name; 362 } 363 364 if (!this.template || isAsianName !== this.isAsianLocale) { 365 temp = isAsianName ? this.defaultAsianTemplate : this.defaultEuroTemplate; 366 } else { 367 temp = this.template; 368 } 369 370 // use the honorific as the prefix or the suffix as appropriate for the order of the name 371 if (modified.honorific) { 372 if ((this.order === 'fg' || isAsianName) && currentLanguage !== "ko") { 373 if (!modified.suffix) { 374 modified.suffix = modified.honorific 375 } 376 } else { 377 if (!modified.prefix) { 378 modified.prefix = modified.honorific 379 } 380 } 381 } 382 383 var parts = { 384 prefix: this.comps["p"] && modified.prefix || "", 385 givenName: this.comps["g"] && modified.givenName || "", 386 middleName: this.comps["m"] && modified.middleName || "", 387 familyName: this.comps["f"] && modified.familyName || "", 388 suffix: this.comps["s"] && modified.suffix || "" 389 }; 390 391 formatted = temp.format(parts); 392 return formatted.replace(/\s+/g, ' ').trim(); 393 } 394 }; 395 396 module.exports = NameFmt; 397