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