marctk/
query.rs

1use crate::{Field, Subfield};
2use std::ops::RangeInclusive;
3
4pub struct FieldQuery {
5    pub field_filter: Box<dyn Fn(&&Field) -> bool>,
6}
7
8impl From<RangeInclusive<i64>> for FieldQuery {
9    fn from(range: RangeInclusive<i64>) -> Self {
10        FieldQuery {
11            field_filter: Box::new(move |f: &&Field| match f.tag().parse::<i64>() {
12                Ok(tag_number) => range.contains(&tag_number),
13                Err(_) => false,
14            }),
15        }
16    }
17}
18
19impl From<&str> for FieldQuery {
20    fn from(spec_input: &str) -> Self {
21        let specs: Vec<String> = spec_input.split(':').map(str::to_owned).collect();
22        FieldQuery {
23            field_filter: Box::new(move |f: &&Field| specs.iter().any(|spec| f.matches_spec(spec))),
24        }
25    }
26}
27
28/// Mutable variant of [`FieldQuery`]
29pub struct FieldQueryMut {
30    pub field_filter: Box<dyn FnMut(&&mut Field) -> bool>,
31}
32
33impl From<RangeInclusive<i64>> for FieldQueryMut {
34    fn from(range: RangeInclusive<i64>) -> Self {
35        FieldQueryMut {
36            field_filter: Box::new(move |f: &&mut Field| match f.tag().parse::<i64>() {
37                Ok(tag_number) => range.contains(&tag_number),
38                Err(_) => false,
39            }),
40        }
41    }
42}
43
44impl From<&str> for FieldQueryMut {
45    fn from(spec_input: &str) -> Self {
46        let specs: Vec<String> = spec_input.split(':').map(str::to_owned).collect();
47        FieldQueryMut {
48            field_filter: Box::new(move |f: &&mut Field| {
49                specs.iter().any(|spec| f.matches_spec(spec))
50            }),
51        }
52    }
53}
54
55#[derive(Clone, Debug)]
56pub struct ComplexSpecification {
57    tag: String,
58    ind1: Option<char>,
59    ind2: Option<char>,
60    subfields: Vec<char>,
61}
62
63impl ComplexSpecification {
64    pub fn subfield_filter(&self) -> impl Fn(&Subfield, &Field) -> bool + use<'_> {
65        |subfield: &Subfield, field: &Field| match subfield.code().chars().next() {
66            Some(first_char) => self.subfields.contains(&first_char) && self.matches_field(field),
67            None => false,
68        }
69    }
70
71    pub fn matches_field(&self, field: &Field) -> bool {
72        field.matches_spec(&self.tag) && self.ind1_matches(field) && self.ind2_matches(field)
73    }
74
75    fn ind1_matches(&self, field: &Field) -> bool {
76        match self.ind1 {
77            Some(i) => {
78                i == '*' // * is a wildcard that matches anything
79                    || field.ind1() == i.to_string()
80                    || field.ind1().trim().is_empty() && i == '_' // _ can represent an empty indicator
81            }
82            None => true,
83        }
84    }
85
86    fn ind2_matches(&self, field: &Field) -> bool {
87        match self.ind2 {
88            Some(i) => {
89                i == '*'
90                    || field.ind2() == i.to_string()
91                    || field.ind1().trim().is_empty() && i == '_'
92            }
93            None => true,
94        }
95    }
96
97    fn parse_indicators_and_subfields(
98        remainder: &mut impl Iterator<Item = char>,
99    ) -> (Option<char>, Option<char>, Vec<char>) {
100        // if first character is '(', that means that
101        // there are indicators specified within the
102        // parens
103        let first = remainder.next();
104        let ind1 = match first {
105            Some('(') => remainder.next(),
106            _ => None,
107        };
108        let ind2 = match first {
109            Some('(') => remainder.next(),
110            _ => None,
111        };
112        if first == Some('(') {
113            remainder.next(); // Consume the closing paren
114        }
115        let subfields: Vec<char> = match first {
116            Some('(') => remainder.collect(),
117            _ => first.into_iter().chain(remainder).collect(),
118        };
119
120        (ind1, ind2, subfields)
121    }
122}
123
124impl From<&str> for ComplexSpecification {
125    fn from(raw_spec: &str) -> ComplexSpecification {
126        let mut rest = raw_spec.chars();
127        let tag = rest.by_ref().take(3).collect::<String>();
128        let (ind1, ind2, subfields) = Self::parse_indicators_and_subfields(&mut rest);
129        ComplexSpecification {
130            tag,
131            ind1,
132            ind2,
133            subfields,
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::Record;
142
143    #[test]
144    fn test_can_filter_by_inclusive_range() {
145        let query = FieldQuery::from(600..=699);
146
147        let record = Record::from_breaker(
148            r#"=600 10$aZhang, Heng, $d 78-139 $v Juvenile literature.
149=650 \0$aEarthquakes $v Juvenile literature.
150=955 \0$a1234"#,
151        )
152        .unwrap();
153        let filter = query.field_filter;
154        let mut filtered = record.fields().iter().filter(filter);
155        assert_eq!(filtered.next().unwrap().tag(), "600");
156        assert_eq!(filtered.next().unwrap().tag(), "650");
157        assert!(filtered.next().is_none());
158    }
159
160    #[test]
161    fn test_can_filter_by_string_slice() {
162        let query = FieldQuery::from("x50");
163
164        let record = Record::from_breaker(
165            r#"=150 \\$aLion
166=450 \\$aPanthera leo
167=550 \\$wg$aPanthera
168=953 \\$axx00$bec11"#,
169        )
170        .unwrap();
171        let filter = query.field_filter;
172        let mut filtered = record.fields().iter().filter(filter);
173        assert_eq!(filtered.next().unwrap().tag(), "150");
174        assert_eq!(filtered.next().unwrap().tag(), "450");
175        assert_eq!(filtered.next().unwrap().tag(), "550");
176        assert!(filtered.next().is_none());
177    }
178
179    #[test]
180    fn test_can_filter_by_string_slice_with_multiple_specs() {
181        let query = FieldQuery::from("600:9XX");
182
183        let record = Record::from_breaker(
184            r#"=600 10$aZhang, Heng, $d 78-139 $v Juvenile literature.
185=650 \0$aEarthquakes $v Juvenile literature.
186=955 \0$a1234"#,
187        )
188        .unwrap();
189        let filter = query.field_filter;
190        let mut filtered = record.fields().iter().filter(filter);
191        assert_eq!(filtered.next().unwrap().tag(), "600");
192        assert_eq!(filtered.next().unwrap().tag(), "955");
193        assert!(filtered.next().is_none());
194    }
195
196    #[test]
197    fn test_if_filter_ignores_junk_and_non_numeric_tags() {
198        let query = FieldQuery::from("6XX:ABC$DEF");
199
200        // Some records have funky tags.
201        let record =
202            Record::from_breaker(r#"=ABC \0$aEarthquakes $v Juvenile literature."#).unwrap();
203        let filter = query.field_filter;
204        let mut filtered = record.fields().iter().filter(filter);
205        assert!(filtered.next().is_none());
206    }
207
208    #[test]
209    fn test_can_create_complex_spec() {
210        let spec = ComplexSpecification::from("245a");
211        assert_eq!(spec.tag, "245");
212        assert_eq!(spec.ind1, None);
213        assert_eq!(spec.ind2, None);
214        assert_eq!(spec.subfields, vec!['a']);
215
216        let indicator_spec = ComplexSpecification::from("6xx(01)av");
217        assert_eq!(indicator_spec.tag, "6xx");
218        assert_eq!(indicator_spec.ind1, Some('0'));
219        assert_eq!(indicator_spec.ind2, Some('1'));
220        assert_eq!(indicator_spec.subfields, vec!['a', 'v']);
221    }
222
223    #[test]
224    fn test_can_create_subfield_filter() {
225        let mut field = Field::new("245").unwrap();
226        let _ = field.add_subfield("a", "My title");
227        let _ = field.add_subfield("b", "My subtitle");
228
229        let spec = ComplexSpecification::from("245a");
230        let mut filtered = field
231            .subfields()
232            .iter()
233            .filter(|sf| spec.subfield_filter()(&sf, &field));
234        assert_eq!(filtered.next().unwrap().code(), "a");
235        assert_eq!(filtered.next(), None);
236    }
237
238    #[test]
239    fn test_can_match_fields_to_complex_specifications() {
240        let mut field = Field::new("245").unwrap();
241        let _ = field.set_ind1("1");
242        let _ = field.set_ind2("4");
243
244        assert!(ComplexSpecification::from("245a").matches_field(&field));
245        assert!(ComplexSpecification::from("2xxa").matches_field(&field));
246        assert!(ComplexSpecification::from("245(14)a").matches_field(&field));
247        assert!(ComplexSpecification::from("245(1*)a").matches_field(&field));
248        assert!(!ComplexSpecification::from("245(00)a").matches_field(&field));
249        assert!(!ComplexSpecification::from("650(14)").matches_field(&field));
250    }
251}