use crate::leader::Leader;
use crate::marc_record::MarcRecord;
use crate::record::Field;
use crate::record_helpers::control_field_char_at;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HoldingsRecord {
pub leader: Leader,
pub control_fields: IndexMap<String, Vec<String>>,
pub fields: IndexMap<String, Vec<Field>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HoldingsType {
SinglePartItem,
SerialItem,
MultipartItem,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AcquisitionStatus {
Other,
ReceivedAndComplete,
OnOrder,
ReceivedAndIncomplete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MethodOfAcquisition {
Unknown,
Free,
Gift,
LegalDeposit,
Membership,
Purchase,
Exchange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Completeness {
Complete,
Incomplete,
Scattered,
NotApplicable,
}
impl HoldingsRecord {
#[must_use]
pub fn new(leader: Leader) -> Self {
HoldingsRecord {
leader,
control_fields: IndexMap::new(),
fields: IndexMap::new(),
}
}
#[must_use]
pub fn builder(leader: Leader) -> HoldingsRecordBuilder {
HoldingsRecordBuilder {
record: HoldingsRecord::new(leader),
}
}
pub fn add_control_field(&mut self, tag: String, value: String) {
self.control_fields.entry(tag).or_default().push(value);
}
#[must_use]
pub fn get_control_field(&self, tag: &str) -> Option<&str> {
self.control_fields
.get(tag)
.and_then(|v| v.first())
.map(String::as_str)
}
pub fn add_location(&mut self, field: Field) {
self.fields
.entry("852".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn locations(&self) -> &[Field] {
self.fields.get("852").map_or(&[], Vec::as_slice)
}
pub fn add_captions_basic(&mut self, field: Field) {
self.fields
.entry("853".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn captions_basic(&self) -> &[Field] {
self.fields.get("853").map_or(&[], Vec::as_slice)
}
pub fn add_captions_supplements(&mut self, field: Field) {
self.fields
.entry("854".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn captions_supplements(&self) -> &[Field] {
self.fields.get("854").map_or(&[], Vec::as_slice)
}
pub fn add_captions_indexes(&mut self, field: Field) {
self.fields
.entry("855".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn captions_indexes(&self) -> &[Field] {
self.fields.get("855").map_or(&[], Vec::as_slice)
}
pub fn add_enumeration_basic(&mut self, field: Field) {
self.fields
.entry("863".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn enumeration_basic(&self) -> &[Field] {
self.fields.get("863").map_or(&[], Vec::as_slice)
}
pub fn add_enumeration_supplements(&mut self, field: Field) {
self.fields
.entry("864".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn enumeration_supplements(&self) -> &[Field] {
self.fields.get("864").map_or(&[], Vec::as_slice)
}
pub fn add_enumeration_indexes(&mut self, field: Field) {
self.fields
.entry("865".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn enumeration_indexes(&self) -> &[Field] {
self.fields.get("865").map_or(&[], Vec::as_slice)
}
pub fn add_textual_holdings_basic(&mut self, field: Field) {
self.fields
.entry("866".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn textual_holdings_basic(&self) -> &[Field] {
self.fields.get("866").map_or(&[], Vec::as_slice)
}
pub fn add_textual_holdings_supplements(&mut self, field: Field) {
self.fields
.entry("867".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn textual_holdings_supplements(&self) -> &[Field] {
self.fields.get("867").map_or(&[], Vec::as_slice)
}
pub fn add_textual_holdings_indexes(&mut self, field: Field) {
self.fields
.entry("868".to_string())
.or_default()
.push(field);
}
#[must_use]
pub fn textual_holdings_indexes(&self) -> &[Field] {
self.fields.get("868").map_or(&[], Vec::as_slice)
}
pub fn add_item_information(&mut self, field: Field) {
self.fields
.entry(field.tag.clone())
.or_default()
.push(field);
}
#[must_use]
pub fn get_item_information(&self, tag: &str) -> Option<&[Field]> {
self.fields.get(tag).map(Vec::as_slice)
}
pub fn add_field(&mut self, field: Field) {
self.fields
.entry(field.tag.clone())
.or_default()
.push(field);
}
#[must_use]
pub fn get_fields(&self, tag: &str) -> Option<&[Field]> {
self.fields.get(tag).map(Vec::as_slice)
}
#[must_use]
pub fn holdings_type(&self) -> HoldingsType {
match self.leader.record_type {
'x' => HoldingsType::SinglePartItem,
'y' => HoldingsType::SerialItem,
'v' => HoldingsType::MultipartItem,
_ => HoldingsType::Unknown,
}
}
#[must_use]
pub fn acquisition_status(&self) -> Option<AcquisitionStatus> {
match control_field_char_at(self, "008", 6)? {
'0' => Some(AcquisitionStatus::Other),
'1' => Some(AcquisitionStatus::ReceivedAndComplete),
'2' => Some(AcquisitionStatus::OnOrder),
'3' => Some(AcquisitionStatus::ReceivedAndIncomplete),
_ => None,
}
}
#[must_use]
pub fn method_of_acquisition(&self) -> Option<MethodOfAcquisition> {
match control_field_char_at(self, "008", 7)? {
'u' => Some(MethodOfAcquisition::Unknown),
'f' => Some(MethodOfAcquisition::Free),
'g' => Some(MethodOfAcquisition::Gift),
'l' => Some(MethodOfAcquisition::LegalDeposit),
'm' => Some(MethodOfAcquisition::Membership),
'p' => Some(MethodOfAcquisition::Purchase),
'e' => Some(MethodOfAcquisition::Exchange),
_ => None,
}
}
#[must_use]
pub fn completeness(&self) -> Option<Completeness> {
match control_field_char_at(self, "008", 16)? {
'1' => Some(Completeness::Complete),
'2' => Some(Completeness::Incomplete),
'3' => Some(Completeness::Scattered),
'4' => Some(Completeness::NotApplicable),
_ => None,
}
}
#[must_use]
pub fn is_serial(&self) -> bool {
self.holdings_type() == HoldingsType::SerialItem
}
#[must_use]
pub fn is_multipart(&self) -> bool {
self.holdings_type() == HoldingsType::MultipartItem
}
}
impl MarcRecord for HoldingsRecord {
fn leader(&self) -> &Leader {
&self.leader
}
fn leader_mut(&mut self) -> &mut Leader {
&mut self.leader
}
fn add_control_field(&mut self, tag: impl Into<String>, value: impl Into<String>) {
self.control_fields
.entry(tag.into())
.or_default()
.push(value.into());
}
fn get_control_field(&self, tag: &str) -> Option<&str> {
self.control_fields
.get(tag)
.and_then(|v| v.first())
.map(String::as_str)
}
fn control_fields_iter(&self) -> Box<dyn Iterator<Item = (&str, &str)> + '_> {
Box::new(self.control_fields.iter().flat_map(|(tag, values)| {
values
.iter()
.map(move |value| (tag.as_str(), value.as_str()))
}))
}
fn get_fields(&self, tag: &str) -> Option<&[Field]> {
self.fields.get(tag).map(std::vec::Vec::as_slice)
}
fn get_field(&self, tag: &str) -> Option<&Field> {
self.fields.get(tag).and_then(|v| v.first())
}
}
#[derive(Debug)]
pub struct HoldingsRecordBuilder {
record: HoldingsRecord,
}
impl HoldingsRecordBuilder {
#[must_use]
pub fn control_field(mut self, tag: String, value: String) -> Self {
self.record.add_control_field(tag, value);
self
}
#[must_use]
pub fn location(mut self, field: Field) -> Self {
self.record.add_location(field);
self
}
#[must_use]
pub fn captions_basic(mut self, field: Field) -> Self {
self.record.add_captions_basic(field);
self
}
#[must_use]
pub fn captions_supplements(mut self, field: Field) -> Self {
self.record.add_captions_supplements(field);
self
}
#[must_use]
pub fn captions_indexes(mut self, field: Field) -> Self {
self.record.add_captions_indexes(field);
self
}
#[must_use]
pub fn enumeration_basic(mut self, field: Field) -> Self {
self.record.add_enumeration_basic(field);
self
}
#[must_use]
pub fn enumeration_supplements(mut self, field: Field) -> Self {
self.record.add_enumeration_supplements(field);
self
}
#[must_use]
pub fn enumeration_indexes(mut self, field: Field) -> Self {
self.record.add_enumeration_indexes(field);
self
}
#[must_use]
pub fn textual_holdings_basic(mut self, field: Field) -> Self {
self.record.add_textual_holdings_basic(field);
self
}
#[must_use]
pub fn textual_holdings_supplements(mut self, field: Field) -> Self {
self.record.add_textual_holdings_supplements(field);
self
}
#[must_use]
pub fn textual_holdings_indexes(mut self, field: Field) -> Self {
self.record.add_textual_holdings_indexes(field);
self
}
#[must_use]
pub fn item_information(mut self, field: Field) -> Self {
self.record.add_item_information(field);
self
}
#[must_use]
pub fn add_field(mut self, field: Field) -> Self {
self.record.add_field(field);
self
}
#[must_use]
pub fn build(self) -> HoldingsRecord {
self.record
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::Subfield;
fn create_test_leader() -> Leader {
Leader {
record_length: 1024,
record_status: 'n',
record_type: 'y',
bibliographic_level: '|',
control_record_type: ' ',
character_coding: ' ',
indicator_count: 2,
subfield_code_count: 2,
data_base_address: 300,
encoding_level: '1',
cataloging_form: 'a',
multipart_level: ' ',
reserved: "4500".to_string(),
}
}
#[test]
fn test_create_holdings_record() {
let leader = create_test_leader();
let record = HoldingsRecord::new(leader);
assert!(record.locations().is_empty());
assert!(record.textual_holdings_basic().is_empty());
}
#[test]
fn test_holdings_record_builder() {
let leader = create_test_leader();
let record = HoldingsRecord::builder(leader)
.control_field(
"008".to_string(),
"000101n| a azannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.get_control_field("008"),
Some("000101n| a azannaabn |a aaa ")
);
}
#[test]
fn test_holdings_type_detection() {
let mut leader = create_test_leader();
leader.record_type = 'y';
let record = HoldingsRecord::new(leader.clone());
assert_eq!(record.holdings_type(), HoldingsType::SerialItem);
assert!(record.is_serial());
leader.record_type = 'v';
let record = HoldingsRecord::new(leader.clone());
assert_eq!(record.holdings_type(), HoldingsType::MultipartItem);
assert!(record.is_multipart());
leader.record_type = 'x';
let record = HoldingsRecord::new(leader);
assert_eq!(record.holdings_type(), HoldingsType::SinglePartItem);
}
#[test]
fn test_acquisition_status_parsing() {
let leader = create_test_leader();
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"0001011 a azannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.acquisition_status(),
Some(AcquisitionStatus::ReceivedAndComplete)
);
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"0001012 a azannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.acquisition_status(),
Some(AcquisitionStatus::OnOrder)
);
let record = HoldingsRecord::builder(leader)
.control_field(
"008".to_string(),
"0001013 a azannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.acquisition_status(),
Some(AcquisitionStatus::ReceivedAndIncomplete)
);
}
#[test]
fn test_method_of_acquisition_parsing() {
let leader = create_test_leader();
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"00010n1pzannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.method_of_acquisition(),
Some(MethodOfAcquisition::Purchase)
);
let record = HoldingsRecord::builder(leader)
.control_field(
"008".to_string(),
"00010n1gzannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.method_of_acquisition(),
Some(MethodOfAcquisition::Gift)
);
}
#[test]
fn test_completeness_parsing() {
let leader = create_test_leader();
let field_008_str = "0000001pzzzzzzzz1 ".to_string();
let record = HoldingsRecord::builder(leader)
.control_field("008".to_string(), field_008_str)
.build();
assert_eq!(record.completeness(), Some(Completeness::Complete));
}
#[test]
fn test_add_location() {
let leader = create_test_leader();
let location = Field {
tag: "852".to_string(),
indicator1: ' ',
indicator2: '1',
subfields: smallvec::smallvec![Subfield {
code: 'b',
value: "Main Library".to_string(),
}],
};
let record = HoldingsRecord::builder(leader).location(location).build();
assert_eq!(record.locations().len(), 1);
assert_eq!(record.locations()[0].tag, "852");
}
#[test]
fn test_add_textual_holdings() {
let leader = create_test_leader();
let holdings = Field {
tag: "866".to_string(),
indicator1: '4',
indicator2: '1',
subfields: smallvec::smallvec![Subfield {
code: 'a',
value: "v.1 (1990)-v.10 (2000)".to_string(),
}],
};
let record = HoldingsRecord::builder(leader)
.textual_holdings_basic(holdings)
.build();
assert_eq!(record.textual_holdings_basic().len(), 1);
assert_eq!(
record.textual_holdings_basic()[0].get_subfield('a'),
Some("v.1 (1990)-v.10 (2000)")
);
}
#[test]
fn test_control_field_operations() {
let leader = create_test_leader();
let mut record = HoldingsRecord::new(leader);
record.add_control_field("001".to_string(), "ocm00123456".to_string());
record.add_control_field("005".to_string(), "20250101".to_string());
assert_eq!(record.get_control_field("001"), Some("ocm00123456"));
assert_eq!(record.get_control_field("005"), Some("20250101"));
}
#[test]
fn test_acquisition_status_none() {
let leader = create_test_leader();
let record = HoldingsRecord::new(leader.clone());
assert_eq!(record.acquisition_status(), None);
let record = HoldingsRecord::builder(leader)
.control_field("008".to_string(), "000".to_string())
.build();
assert_eq!(record.acquisition_status(), None);
}
#[test]
fn test_method_of_acquisition_all_values() {
let leader = create_test_leader();
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"00010n1uzannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.method_of_acquisition(),
Some(MethodOfAcquisition::Unknown)
);
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"00010n1fzannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.method_of_acquisition(),
Some(MethodOfAcquisition::Free)
);
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"00010n1lzannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.method_of_acquisition(),
Some(MethodOfAcquisition::LegalDeposit)
);
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"00010n1mzannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.method_of_acquisition(),
Some(MethodOfAcquisition::Membership)
);
let record = HoldingsRecord::builder(leader)
.control_field(
"008".to_string(),
"00010n1ezannaabn |a aaa ".to_string(),
)
.build();
assert_eq!(
record.method_of_acquisition(),
Some(MethodOfAcquisition::Exchange)
);
}
#[test]
fn test_completeness_all_values() {
let leader = create_test_leader();
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"0000001pzzzzzzzz2 ".to_string(),
)
.build();
assert_eq!(record.completeness(), Some(Completeness::Incomplete));
let record = HoldingsRecord::builder(leader.clone())
.control_field(
"008".to_string(),
"0000001pzzzzzzzz3 ".to_string(),
)
.build();
assert_eq!(record.completeness(), Some(Completeness::Scattered));
let record = HoldingsRecord::builder(leader)
.control_field(
"008".to_string(),
"0000001pzzzzzzzz4 ".to_string(),
)
.build();
assert_eq!(record.completeness(), Some(Completeness::NotApplicable));
}
#[test]
fn test_captions_and_enumeration_fields() {
let leader = create_test_leader();
let caption_field = Field {
tag: "853".to_string(),
indicator1: ' ',
indicator2: '1',
subfields: smallvec::smallvec![Subfield {
code: 'a',
value: "v.".to_string(),
}],
};
let enum_field = Field {
tag: "863".to_string(),
indicator1: ' ',
indicator2: '1',
subfields: smallvec::smallvec![Subfield {
code: 'a',
value: "v.1".to_string(),
}],
};
let record = HoldingsRecord::builder(leader)
.captions_basic(caption_field)
.enumeration_basic(enum_field)
.build();
assert_eq!(record.captions_basic().len(), 1);
assert_eq!(record.enumeration_basic().len(), 1);
assert_eq!(record.captions_basic()[0].tag, "853");
assert_eq!(record.enumeration_basic()[0].tag, "863");
}
#[test]
fn test_item_information_fields() {
let leader = create_test_leader();
let item_876 = Field {
tag: "876".to_string(),
indicator1: ' ',
indicator2: ' ',
subfields: smallvec::smallvec![Subfield {
code: 'p',
value: "12345".to_string(),
}],
};
let item_877 = Field {
tag: "877".to_string(),
indicator1: ' ',
indicator2: ' ',
subfields: smallvec::smallvec![Subfield {
code: 'p',
value: "12346".to_string(),
}],
};
let record = HoldingsRecord::builder(leader)
.item_information(item_876)
.item_information(item_877)
.build();
assert!(record.get_item_information("876").is_some());
assert!(record.get_item_information("877").is_some());
assert_eq!(record.get_item_information("876").unwrap().len(), 1);
}
#[test]
fn test_other_fields() {
let leader = create_test_leader();
let field_500 = Field {
tag: "500".to_string(),
indicator1: ' ',
indicator2: ' ',
subfields: smallvec::smallvec![Subfield {
code: 'a',
value: "General note".to_string(),
}],
};
let record = HoldingsRecord::builder(leader).add_field(field_500).build();
assert!(record.get_fields("500").is_some());
assert_eq!(record.get_fields("500").unwrap()[0].tag, "500");
}
#[test]
fn test_single_part_item_type() {
let mut leader = create_test_leader();
leader.record_type = 'x';
let record = HoldingsRecord::new(leader);
assert_eq!(record.holdings_type(), HoldingsType::SinglePartItem);
assert!(!record.is_serial());
assert!(!record.is_multipart());
}
#[test]
fn test_unknown_holdings_type() {
let mut leader = create_test_leader();
leader.record_type = 'z';
let record = HoldingsRecord::new(leader);
assert_eq!(record.holdings_type(), HoldingsType::Unknown);
}
}