Files
HolodoriCalc/src/lib/data/song-catalog.ts
T
2026-08-29 23:49:56 +09:00

58 lines
2.0 KiB
TypeScript

export type SongSort = 'title' | 'difficulty-asc' | 'difficulty-desc';
export type SongChartSummary = {
songId: string;
title: string;
difficulty: string;
difficultyLevel?: number | null;
scoreCalibrationSource?: 'measured' | 'local-estimated-v1' | string;
releaseTime?: number | null;
};
export type SongOption = {
id: string;
title: string;
difficultyLevel: number | null;
};
const englishTitleCollator = new Intl.Collator('en', {
sensitivity: 'base',
numeric: true
});
function normalizedSearch(value: string): string {
return value.normalize('NFKD').toLocaleLowerCase('en').replace(/[^a-z0-9]+/g, ' ').trim();
}
export function songOptions(
charts: readonly SongChartSummary[],
difficulty: string,
query = '',
sort: SongSort = 'title'
): SongOption[] {
const levelBySongId = new Map(charts
.filter((chart) => chart.difficulty === difficulty)
.map((chart) => [chart.songId, chart.difficultyLevel ?? null]));
const options = Array.from(new Map(charts.map((chart) => [chart.songId, {
id: chart.songId,
title: chart.title,
difficultyLevel: levelBySongId.get(chart.songId) ?? null
}])).values());
const search = normalizedSearch(query);
const filtered = search
? options.filter((song) => normalizedSearch(`${song.title} ${song.id}`).includes(search))
: options;
const titleOrder = (left: SongOption, right: SongOption) =>
englishTitleCollator.compare(left.title, right.title) || left.id.localeCompare(right.id);
return filtered.sort((left, right) => {
if (sort === 'title') return titleOrder(left, right);
if (left.difficultyLevel === null) return right.difficultyLevel === null ? titleOrder(left, right) : 1;
if (right.difficultyLevel === null) return -1;
const leftLevel = left.difficultyLevel;
const rightLevel = right.difficultyLevel;
const levelOrder = sort === 'difficulty-desc' ? rightLevel - leftLevel : leftLevel - rightLevel;
return levelOrder || titleOrder(left, right);
});
}