diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 92e2a41..d72e32b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -63,7 +63,7 @@ This document tracks the implementation status of GEDCOM 5.5.1 specification fea - [x] REFN (supports multiple) - [x] TYPE - [x] RIN -- [x] CHANGE_DATE (basic parsing - stores DATE value, skips TIME and other structure) +- [x] CHANGE_DATE (full support: DATE, TIME, NOTE) - [x] NOTE_STRUCTURE - [x] SOURCE_CITATION - [x] MULTIMEDIA_LINK @@ -124,7 +124,7 @@ This document tracks the implementation status of GEDCOM 5.5.1 specification fea - [x] TYPE - [x] RIN (automated record ID) - [x] RESN (restriction notice) -- [x] CHANGE_DATE (CHAN - stores DATE value) +- [x] CHANGE_DATE (full support: DATE, TIME, NOTE) - [x] NOTE_STRUCTURE (supports multiple notes and note references) - [x] SOURCE_CITATION (supports multiple citations) - [x] MULTIMEDIA_LINK (supports multiple object references) @@ -143,7 +143,7 @@ This document tracks the implementation status of GEDCOM 5.5.1 specification fea - [x] RIN - [x] NOTE_STRUCTURE (supports multiple notes and note references) - [x] SOURCE_CITATION -- [ ] CHANGE_DATE (basic parsing, skips structure) +- [x] CHANGE_DATE (full support: DATE, TIME, NOTE) ### NOTE_RECORD @@ -154,7 +154,7 @@ This document tracks the implementation status of GEDCOM 5.5.1 specification fea - [x] REFN (supports multiple) - [x] TYPE - [x] RIN -- [ ] CHANGE_DATE (basic parsing, skips structure) +- [x] CHANGE_DATE (full support: DATE, TIME, NOTE) ### REPOSITORY_RECORD @@ -167,7 +167,7 @@ This document tracks the implementation status of GEDCOM 5.5.1 specification fea - [x] REFN (supports multiple) - [x] TYPE - [x] RIN -- [x] CHANGE_DATE (basic parsing - stores DATE value, skips TIME and other structure) +- [x] CHANGE_DATE (full support: DATE, TIME, NOTE) ### SOURCE_RECORD @@ -189,7 +189,7 @@ This document tracks the implementation status of GEDCOM 5.5.1 specification fea - [x] REFN (supports multiple) - [x] TYPE - [x] RIN -- [ ] CHANGE_DATE (basic parsing, skips structure) +- [x] CHANGE_DATE (full support: DATE, TIME, NOTE) - [x] NOTE_STRUCTURE (supports multiple notes) - [x] MULTIMEDIA_LINK (OBJE references) @@ -205,7 +205,7 @@ This document tracks the implementation status of GEDCOM 5.5.1 specification fea - [x] RFN (registered RFN) - [x] RIN (automated record ID) - [x] NOTE_STRUCTURE (supports multiple notes and note references) -- [x] CHANGE_DATE (basic parsing - stores DATE value, skips TIME and other structure) +- [x] CHANGE_DATE (full support: DATE, TIME, NOTE) ## Priority Features diff --git a/src/main.rs b/src/main.rs index 3d72b75..3e31f34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -226,13 +226,21 @@ mod tests { // Test @F2@ - has CHANGE_DATE let f2 = &gedcom.families[1]; assert_eq!(f2.xref.to_string(), "@F2@"); - assert_eq!(f2.change_date, Some("13 JUN 2000".to_string())); + assert!(f2.change_date.is_some()); + assert_eq!( + f2.change_date.as_ref().unwrap().date, + Some("13 JUN 2000".to_string()) + ); assert_eq!(f2.automated_record_id, Some("2".to_string())); // Test @F3@ - has CHANGE_DATE let f3 = &gedcom.families[2]; assert_eq!(f3.xref.to_string(), "@F3@"); - assert_eq!(f3.change_date, Some("13 JUN 2000".to_string())); + assert!(f3.change_date.is_some()); + assert_eq!( + f3.change_date.as_ref().unwrap().date, + Some("13 JUN 2000".to_string()) + ); assert_eq!(f3.automated_record_id, Some("3".to_string())); } @@ -276,7 +284,11 @@ mod tests { assert_eq!(subm.automated_record_id.as_deref(), Some("1")); // Check change date - assert_eq!(subm.change_date.as_deref(), Some("7 SEP 2000")); + assert!(subm.change_date.is_some()); + assert_eq!( + subm.change_date.as_ref().unwrap().date, + Some("7 SEP 2000".to_string()) + ); // Check automated record ID // (Duplicate assertion removed) diff --git a/src/types/change_date.rs b/src/types/change_date.rs new file mode 100644 index 0000000..f8a7438 --- /dev/null +++ b/src/types/change_date.rs @@ -0,0 +1,256 @@ +use super::{Line, Note}; +use winnow::prelude::*; + +// CHANGE_DATE:= +// +// n CHAN {0:1} +// +1 DATE {1:1} +// +2 TIME {0:1} +// +1 NOTE {0:M} + +/// Represents a CHANGE_DATE structure in GEDCOM +/// +/// This structure records when a record was last changed, including +/// the date, optional time, and optional notes about the change. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ChangeDate { + /// Date of the change (required if CHAN present) + pub date: Option, + + /// Time of the change (format: HH:MM:SS or HH:MM:SS.fraction) + pub time: Option, + + /// Notes about the change + pub notes: Vec, +} + +impl ChangeDate { + /// Parse a CHANGE_DATE structure (CHAN tag) + /// + /// Expects input positioned at the CHAN line and consumes the entire structure. + pub fn parse(input: &mut &str) -> PResult { + let mut change_date = ChangeDate::default(); + + // Parse the CHAN line + let Ok(chan_line) = Line::peek(input) else { + return Ok(change_date); + }; + + if chan_line.tag != "CHAN" { + return Ok(change_date); + } + + let chan_level = chan_line.level; + let _ = Line::parse(input); + + // Parse child tags (DATE, TIME, NOTE) + while let Ok(line) = Line::peek(input) { + // Stop if we've gone back to the same level or higher + if line.level <= chan_level { + break; + } + + let mut consume = true; + + match line.tag { + "DATE" if line.level == chan_level + 1 => { + change_date.date = Some(line.value.to_string()); + + // After parsing DATE, check for TIME at the next level + let _ = Line::parse(input); + consume = false; + + if let Ok(time_line) = Line::peek(input) { + if time_line.tag == "TIME" && time_line.level == chan_level + 2 { + change_date.time = Some(time_line.value.to_string()); + let _ = Line::parse(input); + } + } + } + "NOTE" if line.level == chan_level + 1 => { + if let Ok(note) = Note::parse(input) { + change_date.notes.push(note); + } + consume = false; + } + _ => { + // Unknown tag - skip it + } + } + + if consume { + let _ = Line::parse(input); + } + } + + Ok(change_date) + } +} + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_change_date_with_date_only() { + let data = vec!["1 CHAN", "2 DATE 13 JUN 2000"].join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("13 JUN 2000".to_string())); + assert_eq!(change_date.time, None); + assert_eq!(change_date.notes.len(), 0); + } + + #[test] + fn test_change_date_with_date_and_time() { + let data = vec!["1 CHAN", "2 DATE 7 SEP 2000", "3 TIME 8:35:36"].join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("7 SEP 2000".to_string())); + assert_eq!(change_date.time, Some("8:35:36".to_string())); + assert_eq!(change_date.notes.len(), 0); + } + + #[test] + fn test_change_date_with_date_time_and_note() { + let data = vec![ + "1 CHAN", + "2 DATE 17 Feb 2003", + "3 TIME 9:55:13", + "2 NOTE Updated to fix typo", + ] + .join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("17 Feb 2003".to_string())); + assert_eq!(change_date.time, Some("9:55:13".to_string())); + assert_eq!(change_date.notes.len(), 1); + assert_eq!( + change_date.notes[0].note, + Some("Updated to fix typo".to_string()) + ); + } + + #[test] + fn test_change_date_with_multiple_notes() { + let data = vec![ + "1 CHAN", + "2 DATE 11 Jan 2001", + "3 TIME 16:00:06", + "2 NOTE First change note", + "2 NOTE Second change note", + ] + .join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("11 Jan 2001".to_string())); + assert_eq!(change_date.time, Some("16:00:06".to_string())); + assert_eq!(change_date.notes.len(), 2); + assert_eq!( + change_date.notes[0].note, + Some("First change note".to_string()) + ); + assert_eq!( + change_date.notes[1].note, + Some("Second change note".to_string()) + ); + } + + #[test] + fn test_change_date_with_note_continuation() { + let data = vec![ + "1 CHAN", + "2 DATE 11 Jan 2001", + "2 NOTE This is a long note", + "3 CONT that continues on the next line", + ] + .join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("11 Jan 2001".to_string())); + assert_eq!(change_date.notes.len(), 1); + assert!(change_date.notes[0] + .note + .as_ref() + .unwrap() + .contains("This is a long note")); + assert!(change_date.notes[0] + .note + .as_ref() + .unwrap() + .contains("that continues on the next line")); + } + + #[test] + fn test_change_date_empty() { + let data = vec!["1 CHAN"].join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, None); + assert_eq!(change_date.time, None); + assert_eq!(change_date.notes.len(), 0); + } + + #[test] + fn test_change_date_with_fractional_seconds() { + let data = vec!["1 CHAN", "2 DATE 1 JAN 1998", "3 TIME 13:57:24.80"].join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("1 JAN 1998".to_string())); + assert_eq!(change_date.time, Some("13:57:24.80".to_string())); + } + + #[test] + fn test_change_date_at_different_levels() { + // Test parsing at level 2 (common for nested structures) + let data = vec!["2 CHAN", "3 DATE 12 MAR 2000", "4 TIME 10:36:02"].join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("12 MAR 2000".to_string())); + assert_eq!(change_date.time, Some("10:36:02".to_string())); + } + + #[test] + fn test_change_date_stops_at_next_record() { + let data = vec!["1 CHAN", "2 DATE 13 JUN 2000", "3 TIME 17:07:32", "1 RIN 3"].join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("13 JUN 2000".to_string())); + assert_eq!(change_date.time, Some("17:07:32".to_string())); + + // Verify that RIN line is still available to parse + let next_line = Line::peek(&mut input).unwrap(); + assert_eq!(next_line.tag, "RIN"); + } + + #[test] + fn test_change_date_with_note_reference() { + let data = vec!["1 CHAN", "2 DATE 7 SEP 2000", "2 NOTE @N1@"].join("\n"); + + let mut input = data.as_str(); + let change_date = ChangeDate::parse(&mut input).unwrap(); + + assert_eq!(change_date.date, Some("7 SEP 2000".to_string())); + assert_eq!(change_date.notes.len(), 1); + assert_eq!(change_date.notes[0].note, Some("@N1@".to_string())); + } +} diff --git a/src/types/family.rs b/src/types/family.rs index 986ddef..77c8bf0 100644 --- a/src/types/family.rs +++ b/src/types/family.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use crate::{ parse, types::{ - AdoptedBy, FamilyEventDetail, Line, Note, Object, Pedigree, SourceCitation, UserReference, - Xref, + AdoptedBy, ChangeDate, FamilyEventDetail, Line, Note, Object, Pedigree, SourceCitation, + UserReference, Xref, }, }; use winnow::prelude::*; @@ -180,8 +180,8 @@ pub struct Family { /// Automated record ID pub automated_record_id: Option, - /// Change date - stores the DATE value from CHAN/DATE - pub change_date: Option, + /// Change date - full CHANGE_DATE structure with DATE, TIME, and NOTE + pub change_date: Option, // Legacy fields used for FAMC/FAMS child-to-family links // These are kept for backward compatibility with Individual parsing @@ -351,19 +351,11 @@ impl Family { consume = false; } "CHAN" => { - // Basic parsing - extract DATE value and skip rest of structure - let chan_level = line.level; - let _ = Line::parse(record); - - // Look for DATE tag at next level - while let Ok(peek) = Line::peek(record) { - if peek.level <= chan_level { - break; + if let Ok(change_date) = ChangeDate::parse(record) { + // Only set if we actually got data + if change_date.date.is_some() || !change_date.notes.is_empty() { + family.change_date = Some(change_date); } - if peek.tag == "DATE" && peek.level == chan_level + 1 { - family.change_date = Some(peek.value.to_string()); - } - let _ = Line::parse(record); } consume = false; } @@ -600,10 +592,40 @@ mod tests { let family = Family::parse(&mut record); assert_eq!(family.xref, Xref::new("@F2@".to_string())); - assert_eq!(family.change_date, Some("13 JUN 2000".to_string())); + assert!(family.change_date.is_some()); + let cd = family.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("13 JUN 2000".to_string())); + assert_eq!(cd.time, Some("17:00:35".to_string())); + assert_eq!(cd.notes.len(), 0); assert_eq!(family.automated_record_id, Some("2".to_string())); } + #[test] + fn parse_fam_record_with_change_date_and_note() { + let data = vec![ + "0 @F2@ FAM", + "1 HUSB @I5@", + "1 CHAN", + "2 DATE 13 JUN 2000", + "3 TIME 17:00:35", + "2 NOTE Marriage certificate updated", + ] + .join("\n"); + let mut record = data.as_str(); + + let family = Family::parse(&mut record); + assert_eq!(family.xref, Xref::new("@F2@".to_string())); + assert!(family.change_date.is_some()); + let cd = family.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("13 JUN 2000".to_string())); + assert_eq!(cd.time, Some("17:00:35".to_string())); + assert_eq!(cd.notes.len(), 1); + assert_eq!( + cd.notes[0].note, + Some("Marriage certificate updated".to_string()) + ); + } + #[test] fn parse_fam_record_with_source_citation() { let data = vec![ diff --git a/src/types/individual/individual.rs b/src/types/individual/individual.rs index 9e02999..0f6b25f 100644 --- a/src/types/individual/individual.rs +++ b/src/types/individual/individual.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::parse; use crate::types::individual::name::*; -use crate::types::{Family, Line, Note, Object, SourceCitation, UserReference, Xref}; +use crate::types::{ChangeDate, Family, Line, Note, Object, SourceCitation, UserReference, Xref}; use winnow::prelude::*; use super::{Adoption, Birth, Christening, Death, IndividualEventDetail, Residence}; @@ -258,8 +258,8 @@ pub struct Individual { /// Automated record ID pub automated_record_id: Option, - /// Change date - stores the DATE value from CHAN/DATE - pub change_date: Option, + /// Change date - full CHANGE_DATE structure with DATE, TIME, and NOTE + pub change_date: Option, /// Submitter references pub submitters: Vec, @@ -713,24 +713,11 @@ impl Individual { individual.automated_record_id = Some(line.value.to_string()); } "CHAN" => { - // Parse CHAN structure to get DATE value - let level = line.level; - let _ = Line::parse(record); // consume CHAN line - - while !record.is_empty() { - let Ok(inner_line) = Line::peek(record) else { - break; - }; - - if inner_line.level <= level { - break; - } - - if inner_line.level == level + 1 && inner_line.tag == "DATE" { - individual.change_date = Some(inner_line.value.to_string()); + if let Ok(change_date) = ChangeDate::parse(record) { + // Only set if we actually got data + if change_date.date.is_some() || !change_date.notes.is_empty() { + individual.change_date = Some(change_date); } - - let _ = Line::parse(record); } parse = false; } @@ -2162,7 +2149,37 @@ mod tests { let mut record = buffer.as_str(); let indi = Individual::parse(&mut record); - assert_eq!(indi.change_date.as_ref().unwrap(), "15 JAN 2024"); + assert!(indi.change_date.is_some()); + let cd = indi.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("15 JAN 2024".to_string())); + assert_eq!(cd.time, Some("14:30:00".to_string())); + assert_eq!(cd.notes.len(), 0); + } + + #[test] + fn test_individual_change_date_with_note() { + let data = vec![ + "0 @I1@ INDI", + "1 NAME Test /Person/", + "1 CHAN", + "2 DATE 15 JAN 2024", + "3 TIME 14:30:00", + "2 NOTE Record updated to fix name typo", + ]; + + let buffer = data.join("\n"); + let mut record = buffer.as_str(); + let indi = Individual::parse(&mut record); + + assert!(indi.change_date.is_some()); + let cd = indi.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("15 JAN 2024".to_string())); + assert_eq!(cd.time, Some("14:30:00".to_string())); + assert_eq!(cd.notes.len(), 1); + assert_eq!( + cd.notes[0].note, + Some("Record updated to fix name typo".to_string()) + ); } #[test] diff --git a/src/types/mod.rs b/src/types/mod.rs index bd7b8a1..1262fd0 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,6 +2,7 @@ // top-level record types mod address; mod adopted_by; +mod change_date; mod character_set; mod corporation; mod datetime; @@ -31,6 +32,7 @@ mod xref; pub use address::*; pub use adopted_by::AdoptedBy; +pub use change_date::ChangeDate; pub use character_set::CharacterSet; pub use datetime::DateTime; pub use event::{EventDetail, EventTypeCitedFrom, FamilyEventDetail}; diff --git a/src/types/multimedia_record.rs b/src/types/multimedia_record.rs index dd1d87d..6ae665b 100644 --- a/src/types/multimedia_record.rs +++ b/src/types/multimedia_record.rs @@ -1,4 +1,4 @@ -use super::{Line, Note, SourceCitation, UserReference, Xref}; +use super::{ChangeDate, Line, Note, SourceCitation, UserReference, Xref}; use crate::parse; use winnow::prelude::*; @@ -40,8 +40,8 @@ pub struct MultimediaRecord { /// Source citations for this multimedia object pub source_citations: Vec, - /// Change date - stores the DATE value from CHAN/DATE (skips TIME and other structure) - pub change_date: Option, + /// Change date - full CHANGE_DATE structure with DATE, TIME, and NOTE + pub change_date: Option, } /// Represents a multimedia file within a MULTIMEDIA_RECORD @@ -140,19 +140,11 @@ impl MultimediaRecord { multimedia.automated_record_id = Some(line.value.to_string()); } "CHAN" => { - // Basic parsing - extract DATE value and skip rest of structure - let chan_level = line.level; - let _ = Line::parse(input); - - // Look for DATE tag at next level - while let Ok(peek) = Line::peek(input) { - if peek.level <= chan_level { - break; + if let Ok(change_date) = ChangeDate::parse(input) { + // Only set if we actually got data + if change_date.date.is_some() || !change_date.notes.is_empty() { + multimedia.change_date = Some(change_date); } - if peek.tag == "DATE" && peek.level == chan_level + 1 { - multimedia.change_date = Some(peek.value.to_string()); - } - let _ = Line::parse(input); } consume = false; } @@ -361,7 +353,11 @@ mod tests { let multimedia = MultimediaRecord::parse(&mut input).unwrap(); assert_eq!(multimedia.files.len(), 1); - assert_eq!(multimedia.change_date, Some("14 JAN 2001".to_string())); + assert!(multimedia.change_date.is_some()); + let cd = multimedia.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("14 JAN 2001".to_string())); + assert_eq!(cd.time, Some("14:10:31".to_string())); + assert_eq!(cd.notes.len(), 0); } #[test] diff --git a/src/types/note_record.rs b/src/types/note_record.rs index 6abaf7a..2877d03 100644 --- a/src/types/note_record.rs +++ b/src/types/note_record.rs @@ -1,4 +1,4 @@ -use super::{Line, SourceCitation, UserReference, Xref}; +use super::{ChangeDate, Line, SourceCitation, UserReference, Xref}; use winnow::prelude::*; // NOTE_RECORD:= @@ -32,8 +32,8 @@ pub struct NoteRecord { /// Automated record ID pub automated_record_id: Option, - /// Change date (basic parsing, skips structure for now) - pub change_date: Option, + /// Change date - full CHANGE_DATE structure with DATE, TIME, and NOTE + pub change_date: Option, } impl NoteRecord { @@ -104,17 +104,11 @@ impl NoteRecord { note_record.automated_record_id = Some(line.value.to_string()); } "CHAN" => { - // For now, just skip CHAN and its children - // TODO: Implement full CHANGE_DATE parsing - let chan_level = line.level; - let _ = Line::parse(input); - - // Skip all child tags - while let Ok(peek) = Line::peek(input) { - if peek.level <= chan_level { - break; + if let Ok(change_date) = ChangeDate::parse(input) { + // Only set if we actually got data + if change_date.date.is_some() || !change_date.notes.is_empty() { + note_record.change_date = Some(change_date); } - let _ = Line::parse(input); } consume = false; } @@ -235,7 +229,11 @@ mod tests { let note = NoteRecord::parse(&mut input).unwrap(); assert_eq!(note.note, "Test note"); - // CHAN is parsed but not fully stored yet + assert!(note.change_date.is_some()); + let cd = note.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("24 MAY 1999".to_string())); + assert_eq!(cd.time, Some("16:39:55".to_string())); + assert_eq!(cd.notes.len(), 0); } #[test] diff --git a/src/types/repository_record.rs b/src/types/repository_record.rs index 95c15ad..caf1b97 100644 --- a/src/types/repository_record.rs +++ b/src/types/repository_record.rs @@ -1,4 +1,4 @@ -use super::{Address, Line, Note, UserReference, Xref}; +use super::{Address, ChangeDate, Line, Note, UserReference, Xref}; use crate::parse; use winnow::prelude::*; @@ -37,8 +37,8 @@ pub struct RepositoryRecord { /// Automated record ID pub automated_record_id: Option, - /// Change date - stores the DATE value from CHAN/DATE (skips TIME and other structure) - pub change_date: Option, + /// Change date - full CHANGE_DATE structure with DATE, TIME, and NOTE + pub change_date: Option, } impl RepositoryRecord { @@ -117,19 +117,11 @@ impl RepositoryRecord { repository.automated_record_id = Some(line.value.to_string()); } "CHAN" => { - // Basic parsing - extract DATE value and skip rest of structure - let chan_level = line.level; - let _ = Line::parse(input); - - // Look for DATE tag at next level - while let Ok(peek) = Line::peek(input) { - if peek.level <= chan_level { - break; + if let Ok(change_date) = ChangeDate::parse(input) { + // Only set if we actually got data + if change_date.date.is_some() || !change_date.notes.is_empty() { + repository.change_date = Some(change_date); } - if peek.tag == "DATE" && peek.level == chan_level + 1 { - repository.change_date = Some(peek.value.to_string()); - } - let _ = Line::parse(input); } consume = false; } @@ -279,7 +271,11 @@ mod tests { let mut input = data.as_str(); let repository = RepositoryRecord::parse(&mut input).unwrap(); - assert_eq!(repository.change_date, Some("12 MAR 2000".to_string())); + assert!(repository.change_date.is_some()); + let cd = repository.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("12 MAR 2000".to_string())); + assert_eq!(cd.time, Some("10:36:02".to_string())); + assert_eq!(cd.notes.len(), 0); } #[test] @@ -319,6 +315,9 @@ mod tests { assert_eq!(repository.notes.len(), 1); assert_eq!(repository.user_reference_numbers.len(), 1); assert_eq!(repository.automated_record_id, Some("1".to_string())); - assert_eq!(repository.change_date, Some("12 MAR 2000".to_string())); + assert!(repository.change_date.is_some()); + let cd = repository.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("12 MAR 2000".to_string())); + assert_eq!(cd.time, Some("10:36:02".to_string())); } } diff --git a/src/types/source_record.rs b/src/types/source_record.rs index fe9bc3f..7cb90fb 100644 --- a/src/types/source_record.rs +++ b/src/types/source_record.rs @@ -1,4 +1,4 @@ -use super::{Line, Note, Object, Xref}; +use super::{ChangeDate, Line, Note, Object, Xref}; use crate::parse; use winnow::prelude::*; @@ -70,8 +70,8 @@ pub struct SourceRecord { /// Automated record ID pub automated_record_id: Option, - /// Change date (not yet fully parsed) - pub change_date: Option, + /// Change date - full CHANGE_DATE structure with DATE, TIME, and NOTE + pub change_date: Option, } /// Data about events recorded in a source @@ -218,17 +218,11 @@ impl SourceRecord { source.automated_record_id = Some(line.value.to_string()); } "CHAN" => { - // For now, just skip CHAN and its children - // TODO: Implement full CHANGE_DATE parsing - let chan_level = line.level; - let _ = Line::parse(input); - - // Skip all child tags - while let Ok(peek) = Line::peek(input) { - if peek.level <= chan_level { - break; + if let Ok(change_date) = ChangeDate::parse(input) { + // Only set if we actually got data + if change_date.date.is_some() || !change_date.notes.is_empty() { + source.change_date = Some(change_date); } - let _ = Line::parse(input); } consume = false; } diff --git a/src/types/submitter.rs b/src/types/submitter.rs index 7ae585d..a166dda 100644 --- a/src/types/submitter.rs +++ b/src/types/submitter.rs @@ -1,4 +1,4 @@ -use crate::types::{Address, Line, Note, Object, Xref}; +use crate::types::{Address, ChangeDate, Line, Note, Object, Xref}; // SUBMITTER_RECORD:= // n @@ SUBM {1:1} @@ -37,8 +37,8 @@ pub struct Submitter { /// Notes pub notes: Vec, - /// Change date - stores the DATE value from CHAN/DATE - pub change_date: Option, + /// Change date - full CHANGE_DATE structure with DATE, TIME, and NOTE + pub change_date: Option, } impl Submitter { @@ -115,24 +115,11 @@ impl Submitter { consume = false; } "CHAN" => { - // Parse CHAN structure to get DATE value - let level = line.level; - let _ = Line::parse(record); - - while !record.is_empty() { - let Ok(inner_line) = Line::peek(record) else { - break; - }; - - if inner_line.level <= level { - break; + if let Ok(change_date) = ChangeDate::parse(record) { + // Only set if we actually got data + if change_date.date.is_some() || !change_date.notes.is_empty() { + submitter.change_date = Some(change_date); } - - if inner_line.level == level + 1 && inner_line.tag == "DATE" { - submitter.change_date = Some(inner_line.value.to_string()); - } - - let _ = Line::parse(record); } consume = false; } @@ -228,7 +215,11 @@ mod tests { assert_eq!(submitter.registered_rfn.as_ref().unwrap(), "123456789"); assert_eq!(submitter.automated_record_id.as_ref().unwrap(), "1"); - assert_eq!(submitter.change_date.as_ref().unwrap(), "7 SEP 2000"); + assert!(submitter.change_date.is_some()); + let cd = submitter.change_date.as_ref().unwrap(); + assert_eq!(cd.date, Some("7 SEP 2000".to_string())); + assert_eq!(cd.time, Some("8:35:36".to_string())); + assert_eq!(cd.notes.len(), 0); assert_eq!(submitter.notes.len(), 1); let note = submitter.notes[0].note.as_ref().unwrap();