/*
* NameFmt.js - Format person names for display
*
* Copyright © 2013-2015, 2018-2019, 2023 JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// !data name
var ilib = require("../index.js");
var Utils = require("./Utils.js");
var Locale = require("./Locale.js");
var IString = require("./IString.js");
var Name = require("./Name.js");
var isPunct = require("./isPunct.js");
/**
* @class
* Creates a formatter that can format person name instances (Name) for display to
* a user. The options may contain the following properties:
*
* <ul>
* <li><i>locale</i> - Use the conventions of the given locale to construct the name format.
* <li><i>style</i> - Format the name with the given style. The value of this property
* should be one of the following strings:
* <ul>
* <li><i>short</i> - Format a short name with just the given and family names. eg. "John Smith"
* <li><i>medium</i> - Format a medium-length name with the given, middle, and family names.
* eg. "John James Smith"
* <li><i>long</i> - Format a long name with all names available in the given name object, including
* prefixes. eg. "Mr. John James Smith"
* <li><i>full</i> - Format a long name with all names available in the given name object, including
* prefixes and suffixes. eg. "Mr. John James Smith, Jr."
* <li><i>formal_short</i> - Format a name with the honorific or prefix/suffix and the family
* name. eg. "Mr. Smith"
* <li><i>formal_long</i> - Format a name with the honorific or prefix/suffix and the
* given and family name. eg. "Mr. John Smith"
* <li><i>familiar</i> - Format a name with the most familiar style that the culture of the locale
* will accept. In some locales, it is not rude to address people you just met by their given name.
* In others, it is rude to address a person in such a familiar style unless you are previously
* invited to do so or unless you have known them for a while. In this case, it will use a more formal
* style, but still as familiar as possible so as not to be rude.
* </ul>
* <li><i>components</i> - Format the name with the given components in the correct
* order for those components. Components are encoded as a string of letters representing
* the desired components:
* <ul>
* <li><i>p</i> - prefixes
* <li><i>g</i> - given name
* <li><i>m</i> - middle names
* <li><i>f</i> - family name
* <li><i>s</i> - suffixes
* <li><i>h</i> - honorifics (selects the prefix or suffix as required by the locale)
* </ul>
* <br>
* For example, the string "pf" would mean to only format any prefixes and family names
* together and leave out all the other parts of the name.<br><br>
* The components can be listed in any order in the string. The <i>components</i> option
* overrides the <i>style</i> option if both are specified.
*
* <li>onLoad - a callback function to call when the locale info object is fully
* loaded. When the onLoad option is given, the localeinfo object will attempt to
* load any missing locale data using the ilib loader callback.
* When the constructor is done (even if the data is already preassembled), the
* onLoad function is called with the current instance as a parameter, so this
* callback can be used with preassembled or dynamic loading or a mix of the two.
*
* <li>sync - tell whether to load any missing locale data synchronously or
* asynchronously. If this option is given as "false", then the "onLoad"
* callback must be given, as the instance returned from this constructor will
* not be usable for a while.
*
* <li><i>loadParams</i> - an object containing parameters to pass to the
* loader callback function when locale data is missing. The parameters are not
* interpretted or modified in any way. They are simply passed along. The object
* may contain any property/value pairs as long as the calling code is in
* agreement with the loader callback function as to what those parameters mean.
* </ul>
*
* Formatting names is a locale-dependent function, as the order of the components
* depends on the locale. The following explains some of the details:<p>
*
* <ul>
* <li>In Western countries, the given name comes first, followed by a space, followed
* by the family name. In Asian countries, the family name comes first, followed immediately
* by the given name with no space. But, that format is only used with Asian names written
* in ideographic characters. In Asian countries, especially ones where both an Asian and
* a Western language are used (Hong Kong, Singapore, etc.), the convention is often to
* follow the language of the name. That is, Asian names are written in Asian style, and
* Western names are written in Western style. This class follows that convention as
* well.
* <li>In other Asian countries, Asian names
* written in Latin script are written with Asian ordering. eg. "Xu Ping-an" instead
* of the more Western order "Ping-an Xu", as the order is thought to go with the style
* that is appropriate for the name rather than the style for the language being written.
* <li>In some Spanish speaking countries, people often take both their maternal and
* paternal last names as their own family name. When formatting a short or medium style
* of that family name, only the paternal name is used. In the long style, all the names
* are used. eg. "Juan Julio Raul Lopez Ortiz" took the name "Lopez" from his father and
* the name "Ortiz" from his mother. His family name would be "Lopez Ortiz". The formatted
* short style of his name would be simply "Juan Lopez" which only uses his paternal
* family name of "Lopez".
* <li>In many Western languages, it is common to use auxillary words in family names. For
* example, the family name of "Ludwig von Beethoven" in German is "von Beethoven", not
* "Beethoven". This class ensures that the family name is formatted correctly with
* all auxillary words.
* </ul>
*
*
* @constructor
* @param {Object} options A set of options that govern how the formatter will behave
*/
var NameFmt = function(options) {
var sync = true;
this.style = "short";
this.loadParams = {};
if (options) {
if (options.locale) {
this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
}
if (options.style) {
this.style = options.style;
}
if (options.components) {
this.components = options.components;
}
if (typeof(options.sync) !== 'undefined') {
sync = !!options.sync;
}
if (typeof(options.loadParams) !== 'undefined') {
this.loadParams = options.loadParams;
}
}
// set up defaults in case we need them
this.defaultEuroTemplate = new IString("{prefix} {givenName} {middleName} {familyName}{suffix}");
this.defaultAsianTemplate = new IString("{prefix}{familyName}{givenName}{middleName}{suffix}");
this.useFirstFamilyName = false;
switch (this.style) {
default:
case "s":
case "short":
this.style = "short";
break;
case "m":
case "medium":
this.style = "medium";
break;
case "l":
case "long":
this.style = "long";
break;
case "f":
case "full":
this.style = "full";
break;
case "fs":
case "formal_short":
this.style = "formal_short";
break;
case "fl":
case "formal_long":
this.style = "formal_long";
break;
case "fam":
case "familiar":
this.style = "familiar";
break;
}
this.locale = this.locale || new Locale();
isPunct._init(sync, this.loadParams, ilib.bind(this, function() {
Utils.loadData({
object: "Name",
locale: this.locale,
name: "name.json",
sync: sync,
loadParams: this.loadParams,
callback: ilib.bind(this, function (info) {
this.info = info || Name.defaultInfo;;
this._init();
if (options && typeof(options.onLoad) === 'function') {
options.onLoad(this);
}
})
});
}));
};
NameFmt.prototype = {
/**
* @private
*/
_init: function() {
var arr;
this.comps = {};
if (this.components) {
var valids = {"p":1,"g":1,"m":1,"f":1,"s":1,"h":1};
arr = this.components.split("");
this.comps = {};
for (var i = 0; i < arr.length; i++) {
if (valids[arr[i].toLowerCase()]) {
this.comps[arr[i].toLowerCase()] = true;
}
}
} else {
var comps = this.info.components[this.style];
if (typeof(comps) === "string") {
comps.split("").forEach(ilib.bind(this, function(c) {
this.comps[c] = true;
}));
} else {
this.comps = comps;
}
}
this.template = new IString(this.info.format);
if (this.locale.language === "es" && (this.style !== "long" && this.style !== "full")) {
this.useFirstFamilyName = true; // in spanish, they have 2 family names, the maternal and paternal
}
this.isAsianLocale = (this.info.nameStyle === "asian");
},
/**
* adjoin auxillary words to their head words
* @private
*/
_adjoinAuxillaries: function (parts, namePrefix) {
var start, i, prefixArray, prefix, prefixLower;
//console.info("_adjoinAuxillaries: finding and adjoining aux words in " + parts.join(' '));
if ( this.info.auxillaries && (parts.length > 2 || namePrefix) ) {
for ( start = 0; start < parts.length-1; start++ ) {
for ( i = parts.length; i > start; i-- ) {
prefixArray = parts.slice(start, i);
prefix = prefixArray.join(' ');
prefixLower = prefix.toLowerCase();
prefixLower = prefixLower.replace(/[,\.]/g, ''); // ignore commas and periods
//console.info("_adjoinAuxillaries: checking aux prefix: '" + prefixLower + "' which is " + start + " to " + i);
if ( prefixLower in this.info.auxillaries ) {
//console.info("Found! Old parts list is " + JSON.stringify(parts));
parts.splice(start, i+1-start, prefixArray.concat(parts[i]));
//console.info("_adjoinAuxillaries: Found! New parts list is " + JSON.stringify(parts));
i = start;
}
}
}
}
//console.info("_adjoinAuxillaries: done. Result is " + JSON.stringify(parts));
return parts;
},
/**
* Return the locale for this formatter instance.
* @return {Locale} the locale instance for this formatter
*/
getLocale: function () {
return this.locale;
},
/**
* Return the style of names returned by this formatter
* @return {string} the style of names returned by this formatter
*/
getStyle: function () {
return this.style;
},
/**
* Return the list of components used to format names in this formatter
* @return {string} the list of components
*/
getComponents: function () {
return this.components;
},
/**
* Format the name for display in the current locale with the options set up
* in the constructor of this formatter instance.<p>
*
* If the name does not contain all the parts required for the style, those parts
* will be left blank.<p>
*
* There are two basic styles of formatting: European, and Asian. If this formatter object
* is set for European style, but an Asian name is passed to the format method, then this
* method will format the Asian name with a generic Asian template. Similarly, if the
* formatter is set for an Asian style, and a European name is passed to the format method,
* the formatter will use a generic European template.<p>
*
* This means it is always safe to format any name with a formatter for any locale. You should
* always get something at least reasonable as output.<p>
*
* @param {Name|Object} name the name instance to format, or an object containing name parts to format
* @return {string|undefined} the name formatted according to the style of this formatter instance
*/
format: function(name) {
var formatted, temp, modified, isAsianName;
var currentLanguage = this.locale.getLanguage();
if (!name || typeof(name) !== 'object') {
return undefined;
}
if (!(name instanceof Name)) {
// if the object is not a name, implicitly convert to a name so that the code below works
name = new Name(name, {locale: this.locale});
}
if ((typeof(name.isAsianName) === 'boolean' && !name.isAsianName) ||
Name._isEuroName([name.givenName, name.middleName, name.familyName].join(""), currentLanguage)) {
isAsianName = false; // this is a euro name, even if the locale is asian
modified = name.clone();
// handle the case where there is no space if there is punctuation in the suffix like ", Phd".
// Otherwise, put a space in to transform "PhD" to " PhD"
/*
console.log("suffix is " + modified.suffix);
if ( modified.suffix ) {
console.log("first char is " + modified.suffix.charAt(0));
console.log("isPunct(modified.suffix.charAt(0)) is " + isPunct(modified.suffix.charAt(0)));
}
*/
if (modified.suffix && isPunct(modified.suffix.charAt(0)) === false) {
modified.suffix = ' ' + modified.suffix;
}
if (this.useFirstFamilyName && name.familyName) {
var familyNameParts = modified.familyName.trim().split(' ');
if (familyNameParts.length > 1) {
familyNameParts = this._adjoinAuxillaries(familyNameParts, name.prefix);
} //in spain and mexico, we parse names differently than in the rest of the world
modified.familyName = familyNameParts[0];
}
modified._joinNameArrays();
} else {
isAsianName = true;
modified = name;
}
if (!this.template || isAsianName !== this.isAsianLocale) {
temp = isAsianName ? this.defaultAsianTemplate : this.defaultEuroTemplate;
} else {
temp = this.template;
}
// use the honorific as the prefix or the suffix as appropriate for the order of the name
if (modified.honorific) {
if ((this.order === 'fg' || isAsianName) && currentLanguage !== "ko") {
if (!modified.suffix) {
modified.suffix = modified.honorific
}
} else {
if (!modified.prefix) {
modified.prefix = modified.honorific
}
}
}
var parts = {
prefix: this.comps["p"] && modified.prefix || "",
givenName: this.comps["g"] && modified.givenName || "",
middleName: this.comps["m"] && modified.middleName || "",
familyName: this.comps["f"] && modified.familyName || "",
suffix: this.comps["s"] && modified.suffix || ""
};
formatted = temp.format(parts);
return formatted.replace(/\s+/g, ' ').trim();
}
};
module.exports = NameFmt;
Source