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