398 lines
12 KiB
JavaScript
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 }
|