feat: canonicalise author names

Extractors wrote whatever the file said, so one person held several rows:
"Sam Newman" beside "Newman, Sam;" beside "Sam Newman.epub", the last because
the upload path never stripped the file extension. Tidy on write, in the
validator and in the uniqueness lookup alike, and merge the rows that collide.
This commit is contained in:
2026-08-15 21:56:35 -04:00
parent 373c96d6d6
commit 8ee406533c
6 changed files with 328 additions and 9 deletions
+54
View File
@@ -3,6 +3,7 @@
import pytest
from chitai.services.matching import (
format_author_name,
normalize_author,
normalize_identifier,
normalize_title,
@@ -92,6 +93,59 @@ class TestNormalizeAuthor:
assert normalize_author("Smith, John, Jr.") == "smith john jr"
class TestFormatAuthorName:
"""What gets stored and shown, as opposed to what gets compared."""
@pytest.mark.parametrize(
("written", "expected"),
[
# A leftover separator from a `DC:creator` list.
("Newman, Sam;", "Sam Newman"),
("Sam Newman ", "Sam Newman"),
(" Dan Vanderkam ", "Dan Vanderkam"),
# `Surname, Given` is how EPUBs file a name, not how anyone reads it.
("Kleppmann, Martin", "Martin Kleppmann"),
("Huxley, Aldous", "Aldous Huxley"),
("Liu, Cixin", "Cixin Liu"),
# An extension carried in from the filename the name was read out of.
("Sam Newman.epub", "Sam Newman"),
("Franz Kafka.mobi", "Franz Kafka"),
("Brian W. Kernighan.epub", "Brian W. Kernighan"),
("", ""),
(None, ""),
],
)
def test_names_are_tidied(self, written: str | None, expected: str) -> None:
assert format_author_name(written) == expected
@pytest.mark.parametrize(
"written",
[
# Two people in one string. Flipping it would invent a third.
"Dave Thomas, Andy Hunt",
"Mark Richards, Neal Ford",
# A compound surname is not recognised, and is left alone rather than
# rearranged wrongly.
"García Márquez, Gabriel",
],
)
def test_an_unrecognised_form_is_left_alone(self, written: str) -> None:
assert format_author_name(written) == written
def test_case_and_accents_belong_to_the_author(self) -> None:
"""Tidying removes what an extractor added; it does not correct spelling."""
assert format_author_name("Michał Płachta.epub") == "Michał Płachta"
assert format_author_name("Steve McConnell") == "Steve McConnell"
@pytest.mark.parametrize(
"written", ["Newman, Sam;", "Sam Newman.epub", "Kleppmann, Martin"]
)
def test_tidying_is_idempotent(self, written: str) -> None:
"""`unique_filter` tidies a name that may already be tidy; it must not drift."""
once = format_author_name(written)
assert format_author_name(once) == once
class TestNormalizeIdentifier:
"""Identifiers only help if the same edition produces the same key."""