use std::fmt::Write;
use crate::error::Result;
use crate::record::Record;
pub fn record_to_csv(record: &Record) -> Result<String> {
records_to_csv(std::slice::from_ref(record))
}
pub fn records_to_csv(records: &[Record]) -> Result<String> {
let mut output = String::new();
writeln!(output, "tag,ind1,ind2,subfield_code,value").ok();
for record in records {
for (tag, values) in &record.control_fields {
for value in values {
let escaped_value = escape_csv_value(value);
writeln!(output, "{tag},,,{escaped_value}").ok();
}
}
for (tag, field_list) in &record.fields {
for field in field_list {
if field.subfields.is_empty() {
writeln!(output, "{tag},{},{},", field.indicator1, field.indicator2).ok();
} else {
for subfield in &field.subfields {
let escaped_value = escape_csv_value(&subfield.value);
writeln!(
output,
"{tag},{},{},{},{}",
field.indicator1, field.indicator2, subfield.code, escaped_value
)
.ok();
}
}
}
}
}
Ok(output)
}
pub fn records_to_csv_filtered<F>(records: &[Record], filter: F) -> Result<String>
where
F: Fn(&str) -> bool,
{
let mut output = String::new();
writeln!(output, "tag,ind1,ind2,subfield_code,value").ok();
for record in records {
for (tag, values) in &record.control_fields {
if filter(tag) {
for value in values {
let escaped_value = escape_csv_value(value);
writeln!(output, "{tag},,,{escaped_value}").ok();
}
}
}
for (tag, field_list) in &record.fields {
if filter(tag) {
for field in field_list {
if field.subfields.is_empty() {
writeln!(output, "{tag},{},{},", field.indicator1, field.indicator2).ok();
} else {
for subfield in &field.subfields {
let escaped_value = escape_csv_value(&subfield.value);
writeln!(
output,
"{tag},{},{},{},{}",
field.indicator1, field.indicator2, subfield.code, escaped_value
)
.ok();
}
}
}
}
}
}
Ok(output)
}
fn escape_csv_value(value: &str) -> String {
if value.contains(',') || value.contains('"') || value.contains('\n') {
format!("\"{}\"", value.replace('"', "\"\""))
} else {
value.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::{Field, Record};
use crate::Leader;
fn make_test_leader() -> Leader {
Leader {
record_length: 1000,
record_status: 'n',
record_type: 'a',
bibliographic_level: 'm',
control_record_type: ' ',
character_coding: 'a',
indicator_count: 2,
subfield_code_count: 2,
data_base_address: 100,
encoding_level: ' ',
cataloging_form: 'a',
multipart_level: ' ',
reserved: "4500".to_string(),
}
}
#[test]
fn test_control_field_csv() {
let mut record = Record::new(make_test_leader());
record.add_control_field("001".to_string(), "12345".to_string());
let csv = records_to_csv(&[record]).expect("Failed to generate CSV");
assert!(csv.contains("001,,,12345"));
assert!(csv.starts_with("tag,ind1,ind2,subfield_code,value\n"));
}
#[test]
fn test_data_field_with_subfields() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Title".to_string());
field.add_subfield('b', "Subtitle".to_string());
record.add_field(field);
let csv = records_to_csv(&[record]).expect("Failed to generate CSV");
assert!(csv.contains("245,1,0,a,Title"));
assert!(csv.contains("245,1,0,b,Subtitle"));
}
#[test]
fn test_csv_escaping() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Title, with comma".to_string());
record.add_field(field);
let csv = records_to_csv(&[record]).expect("Failed to generate CSV");
assert!(csv.contains("245,1,0,a,\"Title, with comma\""));
}
#[test]
fn test_csv_quote_escaping() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Title \"quoted\"".to_string());
record.add_field(field);
let csv = records_to_csv(&[record]).expect("Failed to generate CSV");
assert!(csv.contains("245,1,0,a,\"Title \"\"quoted\"\"\""));
}
#[test]
fn test_csv_filtered() {
let mut record = Record::new(make_test_leader());
record.add_control_field("001".to_string(), "12345".to_string());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Title".to_string());
record.add_field(field);
let mut field2 = Field::new("650".to_string(), ' ', '0');
field2.add_subfield('a', "Subject".to_string());
record.add_field(field2);
let csv =
records_to_csv_filtered(&[record], |tag| tag == "245").expect("Failed to generate CSV");
assert!(csv.contains("245,1,0,a,Title"));
assert!(!csv.contains("650"));
assert!(!csv.contains("001"));
}
#[test]
fn test_multiple_records() {
let mut record1 = Record::new(make_test_leader());
record1.add_control_field("001".to_string(), "11111".to_string());
let mut record2 = Record::new(make_test_leader());
record2.add_control_field("001".to_string(), "22222".to_string());
let csv = records_to_csv(&[record1, record2]).expect("Failed to generate CSV");
assert!(csv.contains("001,,,11111"));
assert!(csv.contains("001,,,22222"));
}
}