Skip to main content

aspero_core/matching/
derivative.rs

1//! Replies, corrections and commentaries: records *about* a work, which
2//! often repeat its title.
3
4use super::Claim;
5use super::text::normalise;
6
7/// Words marking a record as *about* another work rather than the work
8/// itself: replies, corrections, errata, commentaries.
9pub(super) const DERIVATIVE_MARKERS: &[&str] = &[
10    "reply",
11    "erratum",
12    "errata",
13    "corrigendum",
14    "correction",
15    "comment on",
16    "response to",
17    "in response",
18    "faculty opinions recommendation",
19    "retraction of",
20    "expression of concern",
21];
22
23/// Markers that are ordinary words inside a title ("differential response
24/// to corticosteroids", "correction of hyponatraemia"), so mark a
25/// derivative only where it says what it is about: at the start, and
26/// "correction" only as "Correction:" or "Correction to".
27const LEADING_MARKERS: &[&str] = &["comment on", "response to", "in response", "correction"];
28
29/// Whether a title marks a derivative record: "…—Reply", "Correction to:
30/// …", "Comment on “…”", "Faculty Opinions recommendation of …".
31pub fn is_derivative(title: &str) -> bool {
32    derivative_kind(title).is_some()
33}
34
35/// What a derivative record is.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum WorkKind {
39    Reply,
40    Correction,
41    Comment,
42    Retraction,
43    ExpressionOfConcern,
44    Recommendation,
45}
46
47impl WorkKind {
48    /// "reply", "correction", "comment", "retraction", "expression of
49    /// concern" or "recommendation".
50    pub fn as_str(self) -> &'static str {
51        match self {
52            WorkKind::Reply => "reply",
53            WorkKind::Correction => "correction",
54            WorkKind::Comment => "comment",
55            WorkKind::Retraction => "retraction",
56            WorkKind::ExpressionOfConcern => "expression of concern",
57            WorkKind::Recommendation => "recommendation",
58        }
59    }
60}
61
62/// What a derivative record is, by its title, as [`WorkKind::as_str`].
63pub fn derivative_kind(title: &str) -> Option<&'static str> {
64    derivative_work_kind(title).map(WorkKind::as_str)
65}
66
67/// What a derivative record is, by its title.
68pub fn derivative_work_kind(title: &str) -> Option<WorkKind> {
69    let norm = normalise(title);
70    let words = format!(" {norm} ");
71    let leading = |m: &str| match m {
72        "correction" => {
73            norm == "correction"
74                || norm.starts_with("correction to ")
75                || title
76                    .trim_start()
77                    .get(..11)
78                    .is_some_and(|h| h.eq_ignore_ascii_case("correction:"))
79        }
80        _ => words.starts_with(&format!(" {m} ")),
81    };
82    let marker = DERIVATIVE_MARKERS
83        .iter()
84        .find(|m| match LEADING_MARKERS.contains(m) {
85            true => leading(m),
86            false => words.contains(&format!(" {m} ")),
87        })?;
88    Some(match *marker {
89        "reply" | "response to" | "in response" => WorkKind::Reply,
90        "erratum" | "errata" | "corrigendum" | "correction" => WorkKind::Correction,
91        "comment on" => WorkKind::Comment,
92        "retraction of" => WorkKind::Retraction,
93        "expression of concern" => WorkKind::ExpressionOfConcern,
94        _ => WorkKind::Recommendation,
95    })
96}
97
98/// Whether the claim itself cites a derivative (its text or title uses a
99/// marker), in which case derivatives are fair matches.
100pub fn claim_mentions_derivative(claim: &Claim) -> bool {
101    [claim.text.as_deref(), claim.title.as_deref()]
102        .into_iter()
103        .flatten()
104        .any(|text| {
105            let words = format!(" {} ", normalise(text));
106            DERIVATIVE_MARKERS
107                .iter()
108                .any(|m| words.contains(&format!(" {m} ")))
109        })
110}