aspero_engine/
registry.rs1use aspero_core::matching::title_similarity;
6use aspero_core::publication::first_page;
7use aspero_core::{IdKind, PartialDate, ProviderId, SourceRecord, UpdateKind, WorkId};
8
9use crate::{Engine, Entry, LookupReport};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct Disagreement {
16 pub field: &'static str,
18 pub value: String,
20 pub registry_value: String,
22 pub registry: ProviderId,
23}
24
25const SAME_TITLE: f64 = 0.5;
27
28pub fn disagreements(record: &SourceRecord, registry: &SourceRecord) -> Vec<Disagreement> {
32 let (w, r) = (&record.work, ®istry.work);
33 let mut out = Vec::new();
34 let mut differ = |field, value: String, registry_value: String| {
35 out.push(Disagreement {
36 field,
37 value,
38 registry_value,
39 registry: registry.source.clone(),
40 });
41 };
42 if let (Some(a), Some(b)) = (w.issued, r.issued) {
43 let years = |d: PartialDate, others: &[i16]| {
44 std::iter::once(d.year())
45 .chain(others.iter().copied())
46 .collect::<Vec<_>>()
47 };
48 let ours = years(a, &w.other_years);
49 if !years(b, &r.other_years).iter().any(|y| ours.contains(y)) {
50 differ("issued", a.to_string(), b.to_string());
51 }
52 }
53 if let (Some(a), Some(b)) = (&w.volume, &r.volume)
54 && !a.trim().eq_ignore_ascii_case(b.trim())
55 {
56 differ("volume", a.clone(), b.clone());
57 }
58 if let (Some(a), Some(b)) = (&w.page, &r.page)
59 && !first_page(a).eq_ignore_ascii_case(first_page(b))
60 {
61 differ("page", a.clone(), b.clone());
62 }
63 if let (Some(a), Some(b)) = (&w.title, &r.title)
64 && title_similarity(a, b) < SAME_TITLE
65 {
66 differ("title", a.clone(), b.clone());
67 }
68 out
69}
70
71impl Engine {
72 pub(crate) fn registry(&self) -> Option<&Entry> {
74 self.entries
75 .iter()
76 .find(|e| e.provider.capabilities().notices)
77 }
78
79 pub async fn cross_check(&self, report: &mut LookupReport) -> Vec<Vec<Disagreement>> {
84 let mut found = vec![Vec::new(); report.results.len()];
85 let Some(registry) = self.registry() else {
86 return found;
87 };
88 let works = crate::found_records(&report.results, |_| true);
89 for (i, theirs) in self.ask_about(registry, works, &mut report.usage).await {
90 if let Some(ours) = &report.results[i].record {
91 found[i] = disagreements(ours, &theirs);
92 }
93 }
94 found
95 }
96
97 pub(crate) async fn date_notices(&self, report: &mut LookupReport) {
103 let Some(registry) = self.registry() else {
104 return;
105 };
106 let mut notices: Vec<WorkId> = Vec::new();
107 for record in report.results.iter().filter_map(|r| r.record.as_ref()) {
108 for u in &record.work.updates {
109 if let Some(n) = &u.notice
110 && u.kind != UpdateKind::NewVersion
111 && n.kind() == IdKind::Doi
112 && !notices.contains(n)
113 {
114 notices.push(n.clone());
115 }
116 }
117 }
118 if notices.is_empty() {
119 return;
120 }
121 let registered = self
122 .lookup_at(
123 registry,
124 ¬ices,
125 aspero_core::FieldSet::ALL,
126 &mut report.usage,
127 )
128 .await;
129 let first = self.lookup(¬ices, aspero_core::FieldSet::DEFAULT).await;
130 for (p, u) in first.usage {
131 crate::add_usage(&mut report.usage, &p, u);
132 }
133 let appeared: Vec<Option<PartialDate>> = registered
134 .into_iter()
135 .zip(first.results)
136 .map(|((_, theirs), ours)| {
137 let consistent = ours.record.filter(|r| {
138 theirs
139 .as_ref()
140 .is_none_or(|t| disagreements(r, t).is_empty())
141 });
142 [theirs, consistent]
143 .into_iter()
144 .flatten()
145 .filter_map(|r| r.work.issued)
146 .min_by_key(|d| starts(*d))
147 })
148 .collect();
149 for record in report.results.iter_mut().filter_map(|r| r.record.as_mut()) {
150 for u in &mut record.work.updates {
151 let Some(at) = u
152 .notice
153 .as_ref()
154 .and_then(|n| notices.iter().position(|x| x == n))
155 else {
156 continue;
157 };
158 let earlier = |a: &PartialDate| u.date.is_none_or(|d| starts(*a) < starts(d));
159 if let Some(earlier) = appeared[at].filter(earlier) {
160 u.deposited = u.date;
161 u.date = Some(earlier);
162 }
163 }
164 }
165 }
166}
167
168fn starts(d: PartialDate) -> (i16, u8, u8) {
170 (d.year(), d.month().unwrap_or(1), d.day().unwrap_or(1))
171}