feat: split the edition out of a book's title

"Fluent Python, 2nd Edition" is one book with a field for the edition. Left in
the title it also splits the library, since the second edition never looks like
the first. Only a numbered statement is moved, so "Catch 22" keeps its number
and "Global Edition" — which has nowhere to go in an integer column — stays put.
This commit is contained in:
2026-08-15 21:57:17 -04:00
parent f6bb06ac6e
commit e967019964
3 changed files with 180 additions and 3 deletions
+2 -1
View File
@@ -37,7 +37,8 @@ from pathlib import Path
(
Path("tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf"),
2,
"The Project Gutenberg eBook #33283: Calculus Made Easy, 2nd Edition",
# The ", 2nd Edition" is split off into `edition`, not kept in the title.
"The Project Gutenberg eBook #33283: Calculus Made Easy",
["Silvanus Phillips Thompson"],
),
],
+99 -1
View File
@@ -1,7 +1,12 @@
import pytest
from pathlib import Path
from datetime import date
from chitai.services.metadata_extractor import EpubExtractor, Extractor, PdfExtractor
from chitai.services.metadata_extractor import (
EpubExtractor,
Extractor,
PdfExtractor,
split_edition,
)
@pytest.mark.asyncio()
@@ -74,3 +79,96 @@ class TestIdentifierMerging:
metadata = await Extractor.extract_metadata([PDF])
assert "identifiers" not in metadata
class TestSplitEdition:
"""An edition is a field on the book, not part of what the book is called."""
@pytest.mark.parametrize(
("title", "stripped", "edition"),
[
# Every form below is one that turned up in a real library.
("Fluent Python, 2nd Edition", "Fluent Python", 2),
("Building Microservices, 2E", "Building Microservices", 2),
("Digital Image Processing, 4e", "Digital Image Processing", 4),
(
"Network Security Essentials: Applications and Standards/6e",
"Network Security Essentials: Applications and Standards",
6,
),
(
"Refactoring: Improving the Design of Existing Code (2nd edition)",
"Refactoring: Improving the Design of Existing Code",
2,
),
# Ordinal words, including a qualifier sitting inside the statement.
(
"The Art of Computer Programming: Volume 1 / Fundamental Algorithms, Third Edition",
"The Art of Computer Programming: Volume 1 / Fundamental Algorithms",
3,
),
(
"Introduction to the Theory of Computation, Third International Edition",
"Introduction to the Theory of Computation",
3,
),
# Mid-title, before a subtitle and before a trailing author.
(
"How Linux Works, 3rd Edition: What Every Superuser Should Know",
"How Linux Works: What Every Superuser Should Know",
3,
),
(
"Code Complete, 2nd Edition - Steve McConnell",
"Code Complete - Steve McConnell",
2,
),
# An underscore between the number and the "e", beside an unnumbered
# qualifier that has nowhere to go in an integer column and so stays put.
(
"Cryptography and Network Security, Global Edition, 8_e - Stallings",
"Cryptography and Network Security, Global Edition - Stallings",
8,
),
],
)
def test_editions_are_split_out(self, title: str, stripped: str, edition: int) -> None:
assert split_edition(title) == (stripped, edition)
@pytest.mark.parametrize(
"title",
[
# A number alone is never an edition — these are titles.
"Catch 22",
"Fahrenheit 451",
"Blade Runner 2049",
"1984",
"Apollo 13",
"Slaughterhouse 5",
"The Art of Computer Programming: Volume 1",
# "Edition" with no number cannot be stored, so it stays where it can
# still be read.
"Cryptography and Network Security: Principles and Practice, Global Edition",
"Building Microservices",
],
)
def test_titles_are_left_alone(self, title: str) -> None:
assert split_edition(title) == (title, None)
def test_a_title_that_is_only_an_edition_is_kept(self) -> None:
"""Stripping must never leave a book with no title at all."""
assert split_edition("2nd Edition") == ("2nd Edition", None)
@pytest.mark.parametrize("title", ["", None])
def test_nothing_yields_nothing(self, title: str | None) -> None:
assert split_edition(title) == (title, None)
@pytest.mark.asyncio()
class TestEditionFromFiles:
async def test_extraction_moves_the_edition_off_the_title(self) -> None:
"""The PDF fixture calls itself a 2nd edition in its own metadata title."""
metadata = await Extractor.extract_metadata([PDF])
assert metadata["title"] == "The Project Gutenberg eBook #33283: Calculus Made Easy"
assert metadata["edition"] == 2