@soleil-se/app-util
Version:
Utility functions for WebApps, RESTApps and Widgets in Sitevision.
112 lines (95 loc) • 3.9 kB
JavaScript
const charOrder = 'abcdefghijklmnopqrstuvwxyzåäæöø';
function normalizeChar(char) {
// Remove accents from unknown characters (é → e, ñ → n)
return char.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function getCharInfo(char) {
const lowerChar = char.toLowerCase();
let index = charOrder.indexOf(lowerChar);
let isAccented = false;
// If not found, try normalized version
if (index === -1) {
const normalized = normalizeChar(lowerChar);
index = charOrder.indexOf(normalized);
if (index !== -1) {
isAccented = true;
} else {
// Completely unknown character
index = charOrder.length + char.charCodeAt(0);
}
}
return {
baseIndex: index,
isAccented,
isLower: char === char.toLowerCase() && char !== char.toUpperCase(),
accentCode: isAccented ? char.normalize('NFD').charCodeAt(1) || 0 : 0,
};
}
/**
* Compares two strings in a localized manner, taking into account special characters.
* The order is defined as: a-z å ä æ ö ø (case-insensitive, with lowercase before uppercase).
* Characters not in this set are compared based on their Unicode code points after removing accents.
* @param {string} a - The first string to compare.
* @param {string} b - The second string to compare.
* @returns {number} Negative if a < b, positive if a > b, zero if equal.
*/
export function localizedCompare(a, b) {
// Handle null/undefined
if (a == null) return b == null ? 0 : -1;
if (b == null) return 1;
// Ensure strings
const strA = String(a);
const strB = String(b);
const minLength = Math.min(strA.length, strB.length);
for (let i = 0; i < minLength; i += 1) {
const infoA = getCharInfo(strA[i]);
const infoB = getCharInfo(strB[i]);
// 1. Compare base character (case and accent insensitive)
if (infoA.baseIndex !== infoB.baseIndex) {
return infoA.baseIndex - infoB.baseIndex;
}
// 2. Non-accented before accented (e.g., 'e' < 'é')
if (infoA.isAccented !== infoB.isAccented) {
return infoA.isAccented ? 1 : -1;
}
// 3. If both accented, compare accent types (é vs è)
if (infoA.isAccented && infoA.accentCode !== infoB.accentCode) {
return infoA.accentCode - infoB.accentCode;
}
// 4. Lowercase before uppercase (e.g., 'e' < 'E')
if (infoA.isLower !== infoB.isLower) {
return infoA.isLower ? -1 : 1;
}
}
return strA.length - strB.length;
}
/**
* Creates a comparator function for sorting objects by a specific property or properties.
* The property values are compared using localized string comparison.
* When given an array of properties, sorts by the first property, then by the second if equal, etc.
* Each property can be a string or an object with a `key` and optional `order` ('asc' | 'desc', defaults to 'asc').
* @param {string|{key: string, order?: 'asc'|'desc'}|Array<string|{key: string, order?: 'asc'|'desc'}>} prop - The property name(s) to compare by.
* @returns {(a: Object, b: Object) => number} A comparator function that accepts two objects and returns their sort order.
* @example
* arr.sort(localizedCompareBy('title'))
* arr.sort(localizedCompareBy(['lastName', 'firstName']))
* arr.sort(localizedCompareBy({ key: 'firstName', order: 'desc' }))
* arr.sort(localizedCompareBy([{ key: 'lastName', order: 'asc' }, { key: 'firstName', order: 'desc' }]))
*/
export function localizedCompareBy(prop) {
const props = (Array.isArray(prop) ? prop : [prop])
.map((p) => (typeof p === 'string'
? { key: p, order: 'asc' }
: { key: p.key, order: p.order ?? 'asc' }
));
return (a, b) => {
for (let i = 0; i < props.length; i += 1) {
const { key, order } = props[i];
const result = localizedCompare(a[key], b[key]);
if (result !== 0) {
return order === 'desc' ? -result : result;
}
}
return 0;
};
}