diff --git a/bindery/store.py b/bindery/store.py index 3796c05..587b3c7 100644 --- a/bindery/store.py +++ b/bindery/store.py @@ -6,6 +6,7 @@ site without losing text. """ import json +import re import sqlite3 from collections.abc import Iterator from contextlib import contextmanager @@ -120,6 +121,14 @@ def _story(row: sqlite3.Row | None) -> dict[str, Any] | None: return d +_ARTICLE = re.compile(r"^(?:the|an?)\s+", re.I) + + +def sort_title(title: str | None) -> str: + """The key that sorts a story list by title, without a leading "The", "A" or "An".""" + return _ARTICLE.sub("", (title or "").strip()).casefold() + + class Store: def __init__(self, path: Path) -> None: self.path = Path(path) @@ -190,8 +199,8 @@ class Store: AND c.on_site = 0) AS offsite_count, (SELECT COUNT(*) FROM chapters c WHERE c.story_id = s.id AND c.included = 1 AND (SELECT COUNT(*) FROM revisions r WHERE r.chapter_id = c.id) > 1) AS revised_count - FROM stories s ORDER BY s.title COLLATE NOCASE""").fetchall() - return [_story(r) for r in rows] + FROM stories s""").fetchall() + return [_story(r) for r in sorted(rows, key=lambda r: sort_title(r["title"]))] def update_story(self, story_id: int, **fields: Any) -> None: if not fields: diff --git a/tests/test_store.py b/tests/test_store.py index 421b64a..1a6f405 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -47,6 +47,14 @@ def test_list_stories_counts_offsite_and_revised_chapters(tmp_path): assert story["revised_count"] == 1 +def test_list_stories_sorts_titles_without_leading_articles(tmp_path): + store = Store(tmp_path / "db.sqlite") + for n, title in enumerate(["The Zebra", "an Owl", "Anthem", "A Bee", "Theory", "Cat"]): + store.create_story(f"https://example/s/{n}", "example", title, "A", []) + titles = [s["title"] for s in store.list_stories()] + assert titles == ["Anthem", "A Bee", "Cat", "an Owl", "Theory", "The Zebra"] + + def test_cover_replaces_previous_cover(tmp_path): store = Store(tmp_path / "db.sqlite") sid = store.create_story("u", "s", "T", "A", [])