name-match
Version:
A robust name matching library combining multiple algorithms for optimal performance
269 lines (238 loc) • 7.18 kB
JavaScript
/**
* Name normalization utility
*
* Provides functions to clean, normalize, and standardize names
* for comparison and matching.
*/
// Common prefixes and suffixes in names
const PREFIXES = ['mr', 'mrs', 'ms', 'miss', 'dr', 'prof', 'rev', 'hon'];
const SUFFIXES = ['jr', 'sr', 'ii', 'iii', 'iv', 'v', 'md', 'phd', 'esq'];
// Common name variations/nicknames
const NAME_VARIATIONS = {
'william': ['will', 'bill', 'billy', 'willy'],
'robert': ['rob', 'bob', 'bobby'],
'richard': ['rick', 'dick', 'richie', 'ricky'],
'michael': ['mike', 'mikey', 'mick'],
'james': ['jim', 'jimmy', 'jamie'],
'joseph': ['joe', 'joey', 'jo'],
'thomas': ['tom', 'tommy'],
'christopher': ['chris', 'topher'],
'charles': ['chuck', 'charlie', 'chas'],
'daniel': ['dan', 'danny'],
'matthew': ['matt', 'matty'],
'anthony': ['tony', 'ant'],
'steven': ['steve', 'stevie'],
'kenneth': ['ken', 'kenny'],
'edward': ['ed', 'eddie', 'ted', 'teddy'],
'donald': ['don', 'donny'],
'elizabeth': ['liz', 'lizzy', 'beth', 'betty', 'eli'],
'jennifer': ['jen', 'jenny'],
'katherine': ['kathy', 'kate', 'katie', 'katy'],
'margaret': ['maggie', 'meg', 'megan', 'peggy'],
'patricia': ['pat', 'patty', 'trish'],
'deborah': ['deb', 'debbie'],
'jessica': ['jess', 'jessie'],
'sandra': ['sandy'],
'barbara': ['barb', 'barbie'],
'stephanie': ['steph', 'stephy'],
'victoria': ['vicky', 'tori'],
'jonathan': ['jon', 'jonny'],
'nicholas': ['nick', 'nicky'],
'jeffrey': ['jeff'],
'benjamin': ['ben', 'benny'],
'timothy': ['tim', 'timmy'],
'gregory': ['greg', 'gregg'],
'raymond': ['ray'],
'samuel': ['sam', 'sammy'],
'andrew': ['andy', 'drew'],
'alexander': ['alex', 'al'],
'david': ['dave', 'davey'],
'joshua': ['josh']
};
/**
* Clean a name string by removing special characters,
* extra spaces, and normalizing case
*
* @param {string} name - The name to clean
* @returns {string} - Cleaned name
*/
function cleanName(name) {
if (!name) return '';
return name
.toLowerCase()
.replace(/[^\w\s'-]/g, ' ') // Replace special chars with space
.replace(/\s+/g, ' ') // Normalize spaces
.trim();
}
/**
* Reorder name2 if its last part is the first part of name1
* @param {string} name1 - First name
* @param {string} name2 - Second name
* @returns {string} - Reordered name2 if needed
*/
function reorderNameIfNeeded(name1, name2) {
const parts1 = name1.split(' ');
const parts2 = name2.split(' ');
if (parts1.length > 1 && parts2.length > 1 && parts1[parts1.length - 1] === parts2[0]) {
// Move the first part of name2 to the end
const reorderedParts2 = parts2.slice(1).concat(parts2[0]);
return reorderedParts2.join(' ');
}
return name2;
}
/**
* Parse a name into its components
*
* @param {string} name - The name to parse
* @returns {Object} - Parsed name components
*/
function parseName(name) {
if (!name) return { original: '', parts: [] };
// Check for comma format (last, first)
let normalized = name;
if (name.includes(',')) {
const parts = name.split(',').map(p => p.trim());
if (parts.length >= 2) {
normalized = `${parts[1]} ${parts[0]}`;
}
}
// Normalized and clean name
const cleanedName = cleanName(normalized);
if(cleanedName !== normalized) {
normalized = cleanedName;
}
// Split into parts
const allParts = normalized.split(' ');
// Separate prefixes, main parts, and suffixes
const prefixes = [];
const suffixes = [];
const mainParts = [];
for (const part of allParts) {
if (PREFIXES.includes(part.replace(/\.$/, ''))) {
prefixes.push(part);
} else if (SUFFIXES.includes(part.replace(/\.$/, ''))) {
suffixes.push(part);
} else {
mainParts.push(part);
}
}
// Extract name components from main parts
const firstName = mainParts.length > 0 ? mainParts[0] : '';
const lastName = mainParts.length > 1 ? mainParts[mainParts.length - 1] : '';
const middleNames = mainParts.length > 2 ? mainParts.slice(1, -1) : [];
return {
original: name,
cleaned: cleanedName,
normalized,
prefixes,
firstName,
middleNames,
lastName,
suffixes,
initials: {
first: firstName ? firstName[0] : '',
middle: middleNames.map(n => n[0]).join(''),
last: lastName ? lastName[0] : ''
}
};
}
/**
* Standardize a name for comparison
*
* @param {string} name - The name to standardize
* @returns {string} - Standardized name
*/
function standardizeName(name) {
const parsed = parseName(name);
// Use only main name parts in standard order
const nameParts = [
parsed.firstName,
...parsed.middleNames,
parsed.lastName
].filter(Boolean);
return nameParts.join(' ');
}
/**
* Get name variations based on common nicknames
*
* @param {string} name - The name to get variations for
* @returns {Array} - Array of name variations
*/
function getNameVariations(name) {
const parsed = parseName(name);
const variations = [standardizeName(name)];
// Generate first name variations
const firstNameVariations = [parsed.firstName];
// Add known variations
if (NAME_VARIATIONS[parsed.firstName]) {
firstNameVariations.push(...NAME_VARIATIONS[parsed.firstName]);
}
// Check if this is a nickname, add the formal name
for (const [formal, nicknames] of Object.entries(NAME_VARIATIONS)) {
if (nicknames.includes(parsed.firstName)) {
firstNameVariations.push(formal);
}
}
// Generate full name variations
for (const firstVariation of firstNameVariations) {
variations.push([
firstVariation,
...parsed.middleNames,
parsed.lastName
].filter(Boolean).join(' '));
}
// Add variations with middle initial
if (parsed.middleNames.length > 0) {
variations.push([
parsed.firstName,
parsed.initials.middle, // Just middle initials
parsed.lastName
].filter(Boolean).join(' '));
// Also with first name variations
for (const firstVariation of firstNameVariations) {
if (firstVariation !== parsed.firstName) {
variations.push([
firstVariation,
parsed.initials.middle,
parsed.lastName
].filter(Boolean).join(' '));
}
}
}
// Add variations with no middle name/initial
variations.push([
parsed.firstName,
parsed.lastName
].filter(Boolean).join(' '));
// Also with first name variations
for (const firstVariation of firstNameVariations) {
if (firstVariation !== parsed.firstName) {
variations.push([
firstVariation,
parsed.lastName
].filter(Boolean).join(' '));
}
}
// Return unique variations
return [...new Set(variations)];
}
/**
* Normalize name order (handle last, first format)
*
* @param {string} name - The name to normalize
* @returns {string} - Name with standardized order
*/
function normalizeNameOrder(name) {
return parseName(name).normalized;
}
module.exports = {
cleanName,
parseName,
reorderNameIfNeeded,
standardizeName,
getNameVariations,
normalizeNameOrder,
NAME_VARIATIONS,
PREFIXES,
SUFFIXES
};