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;
6use crate::id::starts_with_ci;
7
8/// Words marking a record as *about* another work rather than the work
9/// itself: replies, corrections, errata, commentaries.
10pub(super) const DERIVATIVE_MARKERS: &[&str] = &[
11    "reply",
12    "erratum",
13    "errata",
14    "corrigendum",
15    "correction",
16    "comment on",
17    "response to",
18    "in response",
19    "faculty opinions recommendation",
20    "retraction of",
21    "expression of concern",
22];
23
24/// Markers that are ordinary words inside a title ("differential response
25/// to corticosteroids", "correction of hyponatraemia"), so mark a
26/// derivative only where it says what it is about: at the start, and
27/// "correction" only as "Correction:" or "Correction to".
28const LEADING_MARKERS: &[&str] = &["comment on", "response to", "in response", "correction"];
29
30/// Whether a title marks a derivative record: "…—Reply", "Correction to:
31/// …", "Comment on “…”", "Faculty Opinions recommendation of …".
32pub fn is_derivative(title: &str) -> bool {
33    derivative_kind(title).is_some()
34}
35
36/// What a derivative record is.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum WorkKind {
40    Reply,
41    Correction,
42    Comment,
43    Retraction,
44    ExpressionOfConcern,
45    Recommendation,
46}
47
48impl WorkKind {
49    /// "reply", "correction", "comment", "retraction", "expression of
50    /// concern" or "recommendation".
51    pub fn as_str(self) -> &'static str {
52        match self {
53            WorkKind::Reply => "reply",
54            WorkKind::Correction => "correction",
55            WorkKind::Comment => "comment",
56            WorkKind::Retraction => "retraction",
57            WorkKind::ExpressionOfConcern => "expression of concern",
58            WorkKind::Recommendation => "recommendation",
59        }
60    }
61}
62
63/// What a derivative record is, by its title, as [`WorkKind::as_str`].
64pub fn derivative_kind(title: &str) -> Option<&'static str> {
65    derivative_work_kind(title).map(WorkKind::as_str)
66}
67
68/// What a derivative record is, by its title.
69pub fn derivative_work_kind(title: &str) -> Option<WorkKind> {
70    let norm = normalise(title);
71    let words = format!(" {norm} ");
72    let leading = |m: &str| match m {
73        "correction" => {
74            norm == "correction"
75                || norm.starts_with("correction to ")
76                || starts_with_ci(title.trim_start(), "correction:")
77        }
78        _ => words.starts_with(&format!(" {m} ")),
79    };
80    let marker = DERIVATIVE_MARKERS
81        .iter()
82        .find(|m| match LEADING_MARKERS.contains(m) {
83            true => leading(m),
84            false => words.contains(&format!(" {m} ")),
85        })?;
86    Some(match *marker {
87        "reply" | "response to" | "in response" => WorkKind::Reply,
88        "erratum" | "errata" | "corrigendum" | "correction" => WorkKind::Correction,
89        "comment on" => WorkKind::Comment,
90        "retraction of" => WorkKind::Retraction,
91        "expression of concern" => WorkKind::ExpressionOfConcern,
92        _ => WorkKind::Recommendation,
93    })
94}
95
96/// Whether the claim itself cites a derivative, by the same rule as
97/// records: its title, or a sentence of its text ("Smith J. Correction to:
98/// …"), marks one. Then derivatives are fair matches.
99pub fn claim_mentions_derivative(claim: &Claim) -> bool {
100    claim.title.as_deref().is_some_and(is_derivative)
101        || claim
102            .text
103            .as_deref()
104            .is_some_and(|text| text.split(['.', '?', '!']).any(is_derivative))
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn text(t: &str) -> Claim {
112        Claim {
113            text: Some(t.into()),
114            ..Claim::default()
115        }
116    }
117
118    #[test]
119    fn claims_mention_derivatives_as_records_do() {
120        assert!(claim_mentions_derivative(&text(
121            "Smith J. Correction to: Deep learning. Nature. 2016."
122        )));
123        assert!(claim_mentions_derivative(&text(
124            "LeCun Y. Deep learning—Reply. Nature."
125        )));
126        // Ordinary words in a title, not markers.
127        assert!(!claim_mentions_derivative(&text(
128            "Smith J. Differential response to corticosteroids. Thorax. 2019."
129        )));
130        let title = Claim {
131            title: Some("Correction of hyponatraemia".into()),
132            ..Claim::default()
133        };
134        assert!(!claim_mentions_derivative(&title));
135    }
136}