import type { Book, BookShelf } from '#lib/content'; export interface BookSeriesGroup { name: string; url: string; books: Book[]; counts: Record; } export interface GroupedBooks { series: BookSeriesGroup[]; standalone: Book[]; } const positionGroups = { single: 0, range: 1, none: 2 }; function seriesOrder(first: Book, second: Book): number { const [firstGroup, firstNumber] = positionKey(first); const [secondGroup, secondNumber] = positionKey(second); return firstGroup - secondGroup || firstNumber - secondNumber; } function positionKey(book: Book): [number, number] { const position = book.series?.position ?? ''; const match = position.match(/\d+(?:\.\d+)?/); if (!match) return [positionGroups.none, 0]; const isRange = /\d\s*[-–]\s*\d/.test(position); return [isRange ? positionGroups.range : positionGroups.single, Number(match[0])]; } function shelfCounts(books: Book[]): Record { return { read: books.filter((book) => book.shelf === 'read').length, reading: books.filter((book) => book.shelf === 'reading').length, 'want-to-read': books.filter((book) => book.shelf === 'want-to-read').length }; } export function groupBySeries(books: Book[]): GroupedBooks { const groups = new Map(); const standalone: Book[] = []; for (const book of books) { if (!book.series) { standalone.push(book); continue; } const key = book.series.url || book.series.name; groups.set(key, [...(groups.get(key) ?? []), book]); } const series = [...groups.values()] .map((seriesBooks) => { const ordered = [...seriesBooks].sort(seriesOrder); return { name: ordered[0].series!.name, url: ordered[0].series!.url, books: ordered, counts: shelfCounts(ordered) }; }) .sort((first, second) => second.counts.read - first.counts.read || first.name.localeCompare(second.name)); return { series, standalone }; }