Files

398 lines
12 KiB
JavaScript

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 }