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