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); }); }