use crate::record::Field;
use regex::Regex;
#[derive(Debug, Clone)]
pub struct FieldQuery {
pub tag: Option<String>,
pub indicator1: Option<char>,
pub indicator2: Option<char>,
pub required_subfields: Vec<char>,
}
impl Default for FieldQuery {
fn default() -> Self {
Self::new()
}
}
impl FieldQuery {
#[must_use]
pub fn new() -> Self {
FieldQuery {
tag: None,
indicator1: None,
indicator2: None,
required_subfields: Vec::new(),
}
}
#[must_use]
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
#[must_use]
pub fn indicator1(mut self, indicator: Option<char>) -> Self {
self.indicator1 = indicator;
self
}
#[must_use]
pub fn indicator2(mut self, indicator: Option<char>) -> Self {
self.indicator2 = indicator;
self
}
#[must_use]
pub fn has_subfield(mut self, code: char) -> Self {
if !self.required_subfields.contains(&code) {
self.required_subfields.push(code);
}
self
}
#[must_use]
pub fn has_subfields(mut self, codes: &[char]) -> Self {
for &code in codes {
if !self.required_subfields.contains(&code) {
self.required_subfields.push(code);
}
}
self
}
#[must_use]
pub fn tag_range(self, start_tag: &str, end_tag: &str) -> TagRangeQuery {
TagRangeQuery {
start_tag: start_tag.to_string(),
end_tag: end_tag.to_string(),
indicator1: self.indicator1,
indicator2: self.indicator2,
required_subfields: self.required_subfields,
}
}
#[must_use]
pub fn matches(&self, field: &Field) -> bool {
if let Some(ref tag) = self.tag {
if field.tag != *tag {
return false;
}
}
if let Some(ind1) = self.indicator1 {
if field.indicator1 != ind1 {
return false;
}
}
if let Some(ind2) = self.indicator2 {
if field.indicator2 != ind2 {
return false;
}
}
for &required_code in &self.required_subfields {
if field.get_subfield(required_code).is_none() {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct TagRangeQuery {
pub start_tag: String,
pub end_tag: String,
pub indicator1: Option<char>,
pub indicator2: Option<char>,
pub required_subfields: Vec<char>,
}
impl TagRangeQuery {
#[must_use]
pub fn tag_in_range(&self, tag: &str) -> bool {
tag >= self.start_tag.as_str() && tag <= self.end_tag.as_str()
}
#[must_use]
pub fn matches(&self, field: &Field) -> bool {
if !self.tag_in_range(&field.tag) {
return false;
}
if let Some(ind1) = self.indicator1 {
if field.indicator1 != ind1 {
return false;
}
}
if let Some(ind2) = self.indicator2 {
if field.indicator2 != ind2 {
return false;
}
}
for &required_code in &self.required_subfields {
if field.get_subfield(required_code).is_none() {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SubfieldPatternQuery {
pub tag: String,
pub subfield_code: char,
pattern: Regex,
pub negate: bool,
}
impl SubfieldPatternQuery {
pub fn new(
tag: impl Into<String>,
subfield_code: char,
pattern: &str,
) -> Result<Self, regex::Error> {
Ok(SubfieldPatternQuery {
tag: tag.into(),
subfield_code,
pattern: Regex::new(pattern)?,
negate: false,
})
}
pub fn negated(
tag: impl Into<String>,
subfield_code: char,
pattern: &str,
) -> Result<Self, regex::Error> {
Ok(SubfieldPatternQuery {
tag: tag.into(),
subfield_code,
pattern: Regex::new(pattern)?,
negate: true,
})
}
#[must_use]
pub fn matches(&self, field: &Field) -> bool {
if field.tag != self.tag {
return false;
}
field
.get_subfield(self.subfield_code)
.is_some_and(|value| self.pattern.is_match(value) != self.negate)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SubfieldValueQuery {
pub tag: String,
pub subfield_code: char,
pub value: String,
pub partial: bool,
pub negate: bool,
}
impl SubfieldValueQuery {
#[must_use]
pub fn new(tag: impl Into<String>, subfield_code: char, value: impl Into<String>) -> Self {
SubfieldValueQuery {
tag: tag.into(),
subfield_code,
value: value.into(),
partial: false,
negate: false,
}
}
#[must_use]
pub fn partial(tag: impl Into<String>, subfield_code: char, value: impl Into<String>) -> Self {
SubfieldValueQuery {
tag: tag.into(),
subfield_code,
value: value.into(),
partial: true,
negate: false,
}
}
#[must_use]
pub fn negated(tag: impl Into<String>, subfield_code: char, value: impl Into<String>) -> Self {
SubfieldValueQuery {
tag: tag.into(),
subfield_code,
value: value.into(),
partial: false,
negate: true,
}
}
#[must_use]
pub fn partial_negated(
tag: impl Into<String>,
subfield_code: char,
value: impl Into<String>,
) -> Self {
SubfieldValueQuery {
tag: tag.into(),
subfield_code,
value: value.into(),
partial: true,
negate: true,
}
}
#[must_use]
pub fn matches(&self, field: &Field) -> bool {
if field.tag != self.tag {
return false;
}
field.get_subfield(self.subfield_code).is_some_and(|value| {
let matched = if self.partial {
value.contains(self.value.as_str())
} else {
value == self.value.as_str()
};
matched != self.negate
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::Field;
fn create_test_field(tag: &str, ind1: char, ind2: char, subfields: &[(char, &str)]) -> Field {
let mut field = Field::new(tag.to_string(), ind1, ind2);
for &(code, value) in subfields {
field.add_subfield_str(code, value);
}
field
}
#[test]
fn test_query_matches_tag() {
let field = create_test_field("650", ' ', '0', &[('a', "Subject")]);
let query = FieldQuery::new().tag("650");
assert!(query.matches(&field));
let query = FieldQuery::new().tag("651");
assert!(!query.matches(&field));
}
#[test]
fn test_query_matches_indicator1() {
let field = create_test_field("245", '1', '0', &[('a', "Title")]);
let query = FieldQuery::new().indicator1(Some('1'));
assert!(query.matches(&field));
let query = FieldQuery::new().indicator1(Some('0'));
assert!(!query.matches(&field));
let query = FieldQuery::new().indicator1(None);
assert!(query.matches(&field));
}
#[test]
fn test_query_matches_indicator2() {
let field = create_test_field("650", ' ', '0', &[('a', "Subject")]);
let query = FieldQuery::new().indicator2(Some('0'));
assert!(query.matches(&field));
let query = FieldQuery::new().indicator2(Some('1'));
assert!(!query.matches(&field));
}
#[test]
fn test_query_matches_has_subfield() {
let field = create_test_field("650", ' ', '0', &[('a', "Subject"), ('x', "History")]);
let query = FieldQuery::new().has_subfield('a');
assert!(query.matches(&field));
let query = FieldQuery::new().has_subfield('b');
assert!(!query.matches(&field));
}
#[test]
fn test_query_matches_multiple_subfields() {
let field = create_test_field("650", ' ', '0', &[('a', "Subject"), ('x', "History")]);
let query = FieldQuery::new().has_subfield('a').has_subfield('x');
assert!(query.matches(&field));
let query = FieldQuery::new().has_subfield('a').has_subfield('b');
assert!(!query.matches(&field));
}
#[test]
fn test_query_combines_criteria() {
let field = create_test_field("650", ' ', '0', &[('a', "Subject"), ('x', "History")]);
let query = FieldQuery::new()
.tag("650")
.indicator2(Some('0'))
.has_subfield('a');
assert!(query.matches(&field));
let query = FieldQuery::new()
.tag("651")
.indicator2(Some('0'))
.has_subfield('a');
assert!(!query.matches(&field));
let query = FieldQuery::new()
.tag("650")
.indicator2(Some('1'))
.has_subfield('a');
assert!(!query.matches(&field));
}
#[test]
fn test_tag_range_query_in_range() {
let query = TagRangeQuery {
start_tag: "600".to_string(),
end_tag: "699".to_string(),
indicator1: None,
indicator2: None,
required_subfields: Vec::new(),
};
assert!(query.tag_in_range("600"));
assert!(query.tag_in_range("650"));
assert!(query.tag_in_range("699"));
assert!(!query.tag_in_range("599"));
assert!(!query.tag_in_range("700"));
}
#[test]
fn test_tag_range_query_matches() {
let field = create_test_field("650", ' ', '0', &[('a', "Subject")]);
let query = TagRangeQuery {
start_tag: "600".to_string(),
end_tag: "699".to_string(),
indicator1: None,
indicator2: Some('0'),
required_subfields: vec!['a'],
};
assert!(query.matches(&field));
let field = create_test_field("700", ' ', '0', &[('a', "Name")]);
assert!(!query.matches(&field));
}
#[test]
fn test_field_query_default() {
let query = FieldQuery::default();
let field = create_test_field("650", ' ', '0', &[('a', "Subject")]);
assert!(query.matches(&field));
}
#[test]
fn test_subfield_pattern_query_matches_isbn() {
let field = create_test_field("020", ' ', ' ', &[('a', "978-0-12345-678-9")]);
let query = SubfieldPatternQuery::new("020", 'a', r"^978-.*").unwrap();
assert!(query.matches(&field));
let query = SubfieldPatternQuery::new("020", 'a', r"^979-.*").unwrap();
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_pattern_query_different_tag() {
let field = create_test_field("020", ' ', ' ', &[('a', "978-0-12345-678-9")]);
let query = SubfieldPatternQuery::new("022", 'a', r"^978-.*").unwrap();
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_pattern_query_no_matching_subfield() {
let field = create_test_field("020", ' ', ' ', &[('a', "978-0-12345-678-9")]);
let query = SubfieldPatternQuery::new("020", 'b', r"^978-.*").unwrap();
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_pattern_query_complex_pattern() {
let field = create_test_field("100", ' ', ' ', &[('a', "Smith, John"), ('d', "1873-1944")]);
let query = SubfieldPatternQuery::new("100", 'd', r"\d{4}-\d{4}").unwrap();
assert!(query.matches(&field));
}
#[test]
fn test_subfield_pattern_query_negated_match() {
let field = create_test_field("020", ' ', ' ', &[('a', "978-0-12345-678-9")]);
let query = SubfieldPatternQuery::negated("020", 'a', r"^978-.*").unwrap();
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_pattern_query_negated_nonmatch() {
let field = create_test_field("020", ' ', ' ', &[('a', "978-0-12345-678-9")]);
let query = SubfieldPatternQuery::negated("020", 'a', r"^979-.*").unwrap();
assert!(query.matches(&field));
}
#[test]
fn test_subfield_pattern_query_negated_missing_subfield() {
let field = create_test_field("020", ' ', ' ', &[('a', "978-0-12345-678-9")]);
let query = SubfieldPatternQuery::negated("020", 'z', r".*").unwrap();
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_value_query_exact_match() {
let field = create_test_field("650", ' ', '0', &[('a', "History")]);
let query = SubfieldValueQuery::new("650", 'a', "History");
assert!(query.matches(&field));
let query = SubfieldValueQuery::new("650", 'a', "history");
assert!(!query.matches(&field)); }
#[test]
fn test_subfield_value_query_partial_match() {
let field = create_test_field("650", ' ', '0', &[('a', "World History")]);
let query = SubfieldValueQuery::partial("650", 'a', "History");
assert!(query.matches(&field));
let query = SubfieldValueQuery::partial("650", 'a', "World");
assert!(query.matches(&field));
let query = SubfieldValueQuery::partial("650", 'a', "Medieval");
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_value_query_different_tag() {
let field = create_test_field("650", ' ', '0', &[('a', "History")]);
let query = SubfieldValueQuery::new("651", 'a', "History");
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_value_query_no_subfield() {
let field = create_test_field("650", ' ', '0', &[('a', "History")]);
let query = SubfieldValueQuery::new("650", 'x', "Subdivision");
assert!(!query.matches(&field));
}
#[test]
fn test_subfield_value_query_negated_exact() {
let field = create_test_field("650", ' ', '0', &[('a', "History")]);
let query = SubfieldValueQuery::negated("650", 'a', "History");
assert!(!query.matches(&field));
let query = SubfieldValueQuery::negated("650", 'a', "Science");
assert!(query.matches(&field));
}
#[test]
fn test_subfield_value_query_negated_partial() {
let field = create_test_field("650", ' ', '0', &[('a', "World History")]);
let query = SubfieldValueQuery::partial_negated("650", 'a', "History");
assert!(!query.matches(&field));
let query = SubfieldValueQuery::partial_negated("650", 'a', "Science");
assert!(query.matches(&field));
}
#[test]
fn test_subfield_value_query_negated_missing_subfield() {
let field = create_test_field("650", ' ', '0', &[('a', "History")]);
let query = SubfieldValueQuery::negated("650", 'z', "anything");
assert!(!query.matches(&field));
}
}