import { convertBooleanToSign } from './convert-boolean-to-sign.js' import { compare } from 'natural-orderby' /** * Manages a customizable alphabet for sorting operations. * * Provides methods to generate, modify, and sort alphabets based on various * criteria including Unicode ranges, case prioritization, and locale-specific * ordering. Used by the 'custom' sort type to define character precedence. * * The class supports: * * - Generating alphabets from strings or Unicode ranges * - Case prioritization and manipulation * - Character positioning and removal * - Multiple sorting strategies (natural, locale, char code). * * @example * * ```ts * // Create a custom alphabet with specific character order * const alphabet = Alphabet.generateFrom('aAbBcC') * .prioritizeCase('uppercase') * .getCharacters() * // Returns: 'AaBbCc' * ``` */ var Alphabet = class Alphabet { characters = [] constructor(characters) { this.characters = characters } /** * Generates an alphabet from the given characters. * * @param values - The characters to generate the alphabet from. * @returns - The wrapped alphabet. */ static generateFrom(values) { let arrayValues = typeof values === 'string' ? [...values] : values if (arrayValues.length !== new Set(arrayValues).size) { throw new Error('The alphabet must not contain repeated characters') } if (arrayValues.some(value => value.length !== 1)) { throw new Error('The alphabet must contain single characters') } return new Alphabet( arrayValues.map(value => Alphabet.getCharactersWithCase(value.codePointAt(0)), ), ) } /** * Generates an alphabet containing relevant characters from the Unicode * standard. Contains the Unicode planes 0 and 1. * * @returns - The generated alphabet. */ static generateRecommendedAlphabet() { return Alphabet.generateAlphabetToRange(131072) } /** * Generates an alphabet containing all characters from the Unicode standard * except for irrelevant Unicode planes. Contains the Unicode planes 0, 1, 2 * and 3. * * @returns - The generated alphabet. */ static generateCompleteAlphabet() { return Alphabet.generateAlphabetToRange(262144) } static getCharactersWithCase(codePoint) { let character = String.fromCodePoint(codePoint) let lowercaseCharacter = character.toLowerCase() let uppercaseCharacter = character.toUpperCase() return { value: character, codePoint, ...(lowercaseCharacter === character ? null : ( { lowercaseCharacterCodePoint: lowercaseCharacter.codePointAt(0) } )), ...(uppercaseCharacter === character ? null : ( { uppercaseCharacterCodePoint: uppercaseCharacter.codePointAt(0) } )), } } /** * Generates an alphabet containing relevant characters from the Unicode * standard. * * @param maxCodePoint - The maximum code point to generate the alphabet to. * @returns - The generated alphabet. */ static generateAlphabetToRange(maxCodePoint) { return new Alphabet( Array.from({ length: maxCodePoint }, (_, i) => Alphabet.getCharactersWithCase(i), ), ) } /** * For each character with a lower and upper case, permutes the two cases so * that the alphabet is ordered by the case priority entered. * * @example * * ```ts * Alphabet.generateFrom('aAbBcdCD').prioritizeCase('uppercase') // Returns 'AaBbCDcd'. * ``` * * @param casePriority - The case to prioritize. * @returns - The same alphabet instance with the cases prioritized. */ prioritizeCase(casePriority) { let charactersWithCase = this.getCharactersWithCase() let parsedIndexes = /* @__PURE__ */ new Set() let indexByCodePoints = this.characters.reduce( (indexByCodePoint, character, index) => { indexByCodePoint[character.codePoint] = index return indexByCodePoint }, {}, ) for (let { character, index } of charactersWithCase) { if (parsedIndexes.has(index)) { continue } parsedIndexes.add(index) let otherCharacterIndex = indexByCodePoints[ character.uppercaseCharacterCodePoint ?? character.lowercaseCharacterCodePoint ] if (otherCharacterIndex === void 0) { continue } parsedIndexes.add(otherCharacterIndex) if (!character.uppercaseCharacterCodePoint) { if ( (casePriority === 'uppercase' && index < otherCharacterIndex) || (casePriority === 'lowercase' && index > otherCharacterIndex) ) { continue } } else if ( (casePriority === 'uppercase' && index > otherCharacterIndex) || (casePriority === 'lowercase' && index < otherCharacterIndex) ) { continue } this.characters[index] = this.characters[otherCharacterIndex] this.characters[otherCharacterIndex] = character } return this } /** * Adds specific characters to the end of the alphabet. * * @example * * ```ts * Alphabet.generateFrom('ab').pushCharacters('cd') * // Returns 'abcd' * ``` * * @param values - The characters to push to the alphabet. * @returns - The same alphabet instance without the specified characters. */ pushCharacters(values) { let arrayValues = typeof values === 'string' ? [...values] : values let valuesSet = new Set(arrayValues) let valuesAlreadyExisting = this.characters.filter(({ value }) => valuesSet.has(value), ) if (valuesAlreadyExisting.length > 0) { throw new Error( `The alphabet already contains the characters ${valuesAlreadyExisting .slice(0, 5) .map(({ value }) => value) .join(', ')}`, ) } if (arrayValues.some(value => value.length !== 1)) { throw new Error('Only single characters may be pushed') } this.characters.push( ...[...valuesSet].map(value => Alphabet.getCharactersWithCase(value.codePointAt(0)), ), ) return this } /** * Permutes characters with cases so that all characters with the entered case * are put before the other characters. * * @param caseToComeFirst - The case to put before the other characters. * @returns - The same alphabet instance with all characters with case before * all the characters with the other case. */ placeAllWithCaseBeforeAllWithOtherCase(caseToComeFirst) { let otherCaseKey = caseToComeFirst === 'uppercase' ? 'uppercaseCharacterCodePoint' : 'lowercaseCharacterCodePoint' let keep = [] let move = [] for (let character of this.characters) { ;(character[otherCaseKey] === void 0 ? keep : move).push(character) } this.characters = [...keep, ...move] return this } /** * Places a specific character right before another character in the alphabet. * * @example * * ```ts * Alphabet.generateFrom('ab-cd/').placeCharacterBefore({ * characterBefore: '/', * characterAfter: '-', * }) * // Returns 'ab/-cd' * ``` * * @param params - The parameters for the operation. * @param params.characterBefore - The character to come before * characterAfter. * @param params.characterAfter - The target character. * @returns - The same alphabet instance with the specific character * prioritized. */ placeCharacterBefore({ characterBefore, characterAfter }) { return this.placeCharacterBeforeOrAfter({ characterBefore, characterAfter, type: 'before', }) } /** * Places a specific character right after another character in the alphabet. * * @example * * ```ts * Alphabet.generateFrom('ab-cd/').placeCharacterAfter({ * characterBefore: '/', * characterAfter: '-', * }) * // Returns 'abcd/-' * ``` * * @param params - The parameters for the operation. * @param params.characterBefore - The target character. * @param params.characterAfter - The character to come after characterBefore. * @returns - The same alphabet instance with the specific character * prioritized. */ placeCharacterAfter({ characterBefore, characterAfter }) { return this.placeCharacterBeforeOrAfter({ characterBefore, characterAfter, type: 'after', }) } /** * Removes specific characters from the alphabet by their range. * * @param range - The Unicode range to remove characters from. * @param range.start - The starting Unicode codepoint. * @param range.end - The ending Unicode codepoint. * @returns - The same alphabet instance without the characters from the * specified range. */ removeUnicodeRange({ start, end }) { this.characters = this.characters.filter( ({ codePoint }) => codePoint < start || codePoint > end, ) return this } /** * Sorts the alphabet by the sorting function provided. * * @param sortingFunction - The sorting function to use. * @returns - The same alphabet instance sorted by the sorting function * provided. */ sortBy(sortingFunction) { this.characters.sort((a, b) => sortingFunction(a.value, b.value)) return this } /** * Sorts the alphabet by the natural order of the characters using * `natural-orderby`. * * @param locale - The locale to use for sorting. * @returns - The same alphabet instance sorted by the natural order of the * characters. */ sortByNaturalSort(locale) { let naturalCompare = compare({ locale }) return this.sortBy((a, b) => naturalCompare(a, b)) } /** * Removes specific characters from the alphabet. * * @example * * ```ts * Alphabet.generateFrom('abcd').removeCharacters('dcc') * // Returns 'ab' * ``` * * @param values - The characters to remove from the alphabet. * @returns - The same alphabet instance without the specified characters. */ removeCharacters(values) { this.characters = this.characters.filter( ({ value }) => !values.includes(value), ) return this } /** * Sorts the alphabet by the character code point. * * @returns - The same alphabet instance sorted by the character code point. */ sortByCharCodeAt() { return this.sortBy((a, b) => convertBooleanToSign(a.charCodeAt(0) > b.charCodeAt(0)), ) } /** * Sorts the alphabet by the locale order of the characters. * * @param locales - The locales to use for sorting. * @returns - The same alphabet instance sorted by the locale order of the * characters. */ sortByLocaleCompare(locales) { return this.sortBy((a, b) => a.localeCompare(b, locales)) } /** * Retrieves the characters from the alphabet. * * @returns The characters from the alphabet. */ getCharacters() { return this.characters.map(({ value }) => value).join('') } placeCharacterBeforeOrAfter({ characterBefore, characterAfter, type }) { let indexOfCharacterAfter = this.characters.findIndex( ({ value }) => value === characterAfter, ) let indexOfCharacterBefore = this.characters.findIndex( ({ value }) => value === characterBefore, ) if (indexOfCharacterAfter === -1) { throw new Error(`Character ${characterAfter} not found in alphabet`) } if (indexOfCharacterBefore === -1) { throw new Error(`Character ${characterBefore} not found in alphabet`) } if (indexOfCharacterBefore <= indexOfCharacterAfter) { return this } this.characters.splice( type === 'before' ? indexOfCharacterAfter : indexOfCharacterBefore + 1, 0, this.characters[ type === 'before' ? indexOfCharacterBefore : indexOfCharacterAfter ], ) this.characters.splice( type === 'before' ? indexOfCharacterBefore + 1 : indexOfCharacterAfter, 1, ) return this } getCharactersWithCase() { return this.characters .map((character, index) => { if ( !character.uppercaseCharacterCodePoint && !character.lowercaseCharacterCodePoint ) { return null } return { character, index, } }) .filter(element => element !== null) } } export { Alphabet }