mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-23 03:07:13 +00:00
148 lines
5.0 KiB
Rust
148 lines
5.0 KiB
Rust
//! The series a book belongs to, and which volume of it this is.
|
|||
|
|
//!
|
||
|
|
//! A shelf groups by series and volume, and most catalogues have neither set
|
||
|
|
//! on most books. The consequence is visible rather than theoretical: four
|
||
|
|
//! records all reading "A History of Philosophy" with nothing to say which
|
||
|
|
//! volume any of them is, and five volumes of one work sorting as five
|
||
|
|
//! unrelated books.
|
||
|
|
//!
|
||
|
|
//! Two shapes are recoverable from text the catalogue already holds, and they
|
||
|
|
//! are not the same kind of fact.
|
||
|
|
//!
|
||
|
|
//! A **volume marker** in the title — "The Philokalia Vol 3", "Collected
|
||
|
|
//! Works, Volume 9", "(Raven Son Book 1)" — is a *position*, and gives both a
|
||
|
|
//! series and a number.
|
||
|
|
//!
|
||
|
|
//! **Publisher shorthand** — a title of "Aquinas (Companion)" against an
|
||
|
|
//! author of "Cambridge" — is only *membership*. Cambridge Companions are not
|
||
|
|
//! numbered, so filing one under a numbered series makes every one of them
|
||
|
|
//! display as volume one, asserting an order the data does not have. Those
|
||
|
|
//! get a series and no number.
|
||
|
|
|
||
|
|
pub mod publisher;
|
||
|
|
pub mod volume;
|
||
|
|
|
||
|
|
use schemars::JsonSchema;
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
|
||
|
|
pub use publisher::publisher_series;
|
||
|
|
pub use volume::{parse_volume, roman_or_int};
|
||
|
|
|
||
|
|
/// What a title turned out to say about its series.
|
||
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||
|
|
pub struct Proposal {
|
||
|
|
/// Record version.
|
||
|
|
#[serde(default = "version")]
|
||
|
|
pub v: u32,
|
||
|
|
/// The title it was read from.
|
||
|
|
pub title: String,
|
||
|
|
/// The author it was read against, where one was given.
|
||
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||
|
|
pub author: Option<String>,
|
||
|
|
/// The series the book belongs to.
|
||
|
|
pub series: String,
|
||
|
|
/// Which volume, where the title said. Absent for a series that is
|
||
|
|
/// membership rather than position.
|
||
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||
|
|
pub volume: Option<u32>,
|
||
|
|
/// A title reconstructed from the shorthand, where one reliably can be.
|
||
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||
|
|
pub proposed_title: Option<String>,
|
||
|
|
/// Which of the two shapes this was.
|
||
|
|
pub method: Method,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// How a series was arrived at.
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||
|
|
#[serde(rename_all = "snake_case")]
|
||
|
|
pub enum Method {
|
||
|
|
/// A volume marker in the title: a position within a series.
|
||
|
|
Volume,
|
||
|
|
/// Publisher shorthand in a parenthetical: membership, not position.
|
||
|
|
Publisher,
|
||
|
|
}
|
||
|
|
|
||
|
|
fn version() -> u32 {
|
||
|
|
corpus_core::V
|
||
|
|
}
|
||
|
|
|
||
|
|
impl corpus_core::Record for Proposal {
|
||
|
|
fn version(&self) -> u32 {
|
||
|
|
self.v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// What a title and author say about the series, if anything.
|
||
|
|
///
|
||
|
|
/// The volume marker is looked for first: it carries a number, and a number
|
||
|
|
/// is worth more than membership.
|
||
|
|
pub fn series_of(title: &str, author: Option<&str>) -> Option<Proposal> {
|
||
|
|
if let Some((series, volume)) = parse_volume(title) {
|
||
|
|
return Some(Proposal {
|
||
|
|
v: corpus_core::V,
|
||
|
|
title: title.to_string(),
|
||
|
|
author: author.map(str::to_string),
|
||
|
|
series,
|
||
|
|
volume: Some(volume),
|
||
|
|
proposed_title: None,
|
||
|
|
method: Method::Volume,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
let (proposed_title, series) = publisher_series(title, author?)?;
|
||
|
|
Some(Proposal {
|
||
|
|
v: corpus_core::V,
|
||
|
|
title: title.to_string(),
|
||
|
|
author: author.map(str::to_string),
|
||
|
|
series,
|
||
|
|
volume: None,
|
||
|
|
proposed_title,
|
||
|
|
method: Method::Publisher,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a_volume_marker_gives_a_series_and_a_number() {
|
||
|
|
let found = series_of("The Philokalia Vol 3", None).expect("a series");
|
||
|
|
assert_eq!(found.series, "The Philokalia");
|
||
|
|
assert_eq!(found.volume, Some(3));
|
||
|
|
assert_eq!(found.method, Method::Volume);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn publisher_shorthand_gives_membership_and_no_number() {
|
||
|
|
let found = series_of("Aquinas (Companion)", Some("Cambridge")).expect("a series");
|
||
|
|
assert_eq!(found.series, "Cambridge Companions");
|
||
|
|
assert_eq!(found.volume, None, "membership is not a position");
|
||
|
|
assert_eq!(
|
||
|
|
found.proposed_title.as_deref(),
|
||
|
|
Some("The Cambridge Companion to Aquinas")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a_volume_marker_wins_over_a_parenthetical() {
|
||
|
|
// Both could match; the one carrying a number is worth more.
|
||
|
|
let found = series_of("Aquinas (Companion) Vol 2", Some("Cambridge")).expect("a series");
|
||
|
|
assert_eq!(found.method, Method::Volume);
|
||
|
|
assert_eq!(found.volume, Some(2));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn an_ordinary_title_proposes_nothing() {
|
||
|
|
assert_eq!(series_of("Being and Time", Some("Heidegger")), None);
|
||
|
|
assert_eq!(series_of("", None), None);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn publisher_shorthand_needs_the_publisher() {
|
||
|
|
// The same title against anybody else is just a title.
|
||
|
|
assert_eq!(series_of("Aquinas (Companion)", Some("Kerr")), None);
|
||
|
|
assert_eq!(series_of("Aquinas (Companion)", None), None);
|
||
|
|
}
|
||
|
|
}
|