61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
import type { Book, BookShelf } from '#lib/content';
|
||
|
||
export interface BookSeriesGroup {
|
||
name: string;
|
||
url: string;
|
||
books: Book[];
|
||
counts: Record<BookShelf, number>;
|
||
}
|
||
|
||
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<BookShelf, number> {
|
||
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<string, Book[]>();
|
||
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 };
|
||
}
|