use crate::{
parser::{parse_subset, Parser},
tokenizer::Tokenizer,
types::{date::Date, note::Note},
GedcomError,
};
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct SortDate {
pub value: Option<String>,
pub time: Option<String>,
pub phrase: Option<String>,
}
impl SortDate {
pub fn new(tokenizer: &mut Tokenizer, level: u8) -> Result<SortDate, GedcomError> {
let mut sort_date = SortDate::default();
sort_date.parse(tokenizer, level)?;
Ok(sort_date)
}
#[must_use]
pub fn with_value(value: &str) -> Self {
SortDate {
value: Some(value.to_string()),
..Default::default()
}
}
}
impl Parser for SortDate {
fn parse(&mut self, tokenizer: &mut Tokenizer, level: u8) -> Result<(), GedcomError> {
self.value = Some(tokenizer.take_line_value()?);
let handle_subset = |tag: &str, tokenizer: &mut Tokenizer| -> Result<(), GedcomError> {
match tag {
"TIME" => self.time = Some(tokenizer.take_line_value()?),
"PHRASE" => self.phrase = Some(tokenizer.take_line_value()?),
_ => {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Unhandled SortDate Tag: {tag}"),
})
}
}
Ok(())
};
parse_subset(tokenizer, level, handle_subset)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct CreationDate {
pub date: Option<Date>,
}
impl CreationDate {
pub fn new(tokenizer: &mut Tokenizer, level: u8) -> Result<CreationDate, GedcomError> {
let mut creation_date = CreationDate::default();
creation_date.parse(tokenizer, level)?;
Ok(creation_date)
}
}
impl Parser for CreationDate {
fn parse(&mut self, tokenizer: &mut Tokenizer, level: u8) -> Result<(), GedcomError> {
tokenizer.next_token()?;
let handle_subset = |tag: &str, tokenizer: &mut Tokenizer| -> Result<(), GedcomError> {
match tag {
"DATE" => self.date = Some(Date::new(tokenizer, level + 1)?),
_ => {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Unhandled CreationDate Tag: {tag}"),
})
}
}
Ok(())
};
parse_subset(tokenizer, level, handle_subset)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct Crop {
pub top: Option<f32>,
pub left: Option<f32>,
pub height: Option<f32>,
pub width: Option<f32>,
}
impl Crop {
pub fn new(tokenizer: &mut Tokenizer, level: u8) -> Result<Crop, GedcomError> {
let mut crop = Crop::default();
crop.parse(tokenizer, level)?;
Ok(crop)
}
#[must_use]
pub fn with_dimensions(top: f32, left: f32, height: f32, width: f32) -> Self {
Crop {
top: Some(top),
left: Some(left),
height: Some(height),
width: Some(width),
}
}
#[must_use]
pub fn is_full_image(&self) -> bool {
let top = self.top.unwrap_or(0.0);
let left = self.left.unwrap_or(0.0);
let height = self.height.unwrap_or(100.0);
let width = self.width.unwrap_or(100.0);
(top - 0.0).abs() < f32::EPSILON
&& (left - 0.0).abs() < f32::EPSILON
&& (height - 100.0).abs() < f32::EPSILON
&& (width - 100.0).abs() < f32::EPSILON
}
#[must_use]
pub fn is_valid(&self) -> bool {
let top = self.top.unwrap_or(0.0);
let left = self.left.unwrap_or(0.0);
let height = self.height.unwrap_or(100.0);
let width = self.width.unwrap_or(100.0);
(0.0..=100.0).contains(&top)
&& (0.0..=100.0).contains(&left)
&& (0.0..=100.0).contains(&height)
&& (0.0..=100.0).contains(&width)
&& top + height <= 100.0
&& left + width <= 100.0
}
}
impl Parser for Crop {
fn parse(&mut self, tokenizer: &mut Tokenizer, level: u8) -> Result<(), GedcomError> {
tokenizer.next_token()?;
let handle_subset = |tag: &str, tokenizer: &mut Tokenizer| -> Result<(), GedcomError> {
let value_str = tokenizer.take_line_value()?;
let value: f32 = value_str
.parse()
.map_err(|_| GedcomError::InvalidValueFormat {
line: tokenizer.line as usize,
value: value_str.clone(),
expected_format: "numeric value (0-100)".to_string(),
})?;
match tag {
"TOP" => self.top = Some(value),
"LEFT" => self.left = Some(value),
"HEIGHT" => self.height = Some(value),
"WIDTH" => self.width = Some(value),
_ => {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Unhandled Crop Tag: {tag}"),
})
}
}
Ok(())
};
parse_subset(tokenizer, level, handle_subset)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct NonEvent {
pub event_type: String,
pub date: Option<Date>,
pub note: Option<Note>,
pub source_citations: Vec<crate::types::source::citation::Citation>,
}
impl NonEvent {
pub fn new(tokenizer: &mut Tokenizer, level: u8) -> Result<NonEvent, GedcomError> {
let mut non_event = NonEvent::default();
non_event.parse(tokenizer, level)?;
Ok(non_event)
}
#[must_use]
pub fn for_event(event_type: &str) -> Self {
NonEvent {
event_type: event_type.to_string(),
..Default::default()
}
}
#[must_use]
pub fn event_description(&self) -> &str {
match self.event_type.as_str() {
"MARR" => "Marriage",
"CHR" => "Christening",
"BAPM" => "Baptism",
"BURI" => "Burial",
"CREM" => "Cremation",
"DEAT" => "Death",
"BIRT" => "Birth",
"CENS" => "Census",
"EMIG" => "Emigration",
"IMMI" => "Immigration",
"NATU" => "Naturalization",
"RESI" => "Residence",
_ => &self.event_type,
}
}
}
impl Parser for NonEvent {
fn parse(&mut self, tokenizer: &mut Tokenizer, level: u8) -> Result<(), GedcomError> {
self.event_type = tokenizer.take_line_value()?;
let handle_subset = |tag: &str, tokenizer: &mut Tokenizer| -> Result<(), GedcomError> {
match tag {
"DATE" => self.date = Some(Date::new(tokenizer, level + 1)?),
"NOTE" => self.note = Some(Note::new(tokenizer, level + 1)?),
"SOUR" => {
self.source_citations
.push(crate::types::source::citation::Citation::new(
tokenizer,
level + 1,
)?);
}
_ => {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Unhandled NonEvent Tag: {tag}"),
})
}
}
Ok(())
};
parse_subset(tokenizer, level, handle_subset)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub struct Phrase {
pub value: String,
}
impl Phrase {
pub fn new(tokenizer: &mut Tokenizer, _level: u8) -> Result<Phrase, GedcomError> {
Ok(Phrase {
value: tokenizer.take_line_value()?,
})
}
#[must_use]
pub fn with_value(value: &str) -> Self {
Phrase {
value: value.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sort_date_with_value() {
let sort_date = SortDate::with_value("1818");
assert_eq!(sort_date.value, Some("1818".to_string()));
assert!(sort_date.time.is_none());
assert!(sort_date.phrase.is_none());
}
#[test]
fn test_crop_with_dimensions() {
let crop = Crop::with_dimensions(10.0, 15.0, 50.0, 40.0);
assert_eq!(crop.top, Some(10.0));
assert_eq!(crop.left, Some(15.0));
assert_eq!(crop.height, Some(50.0));
assert_eq!(crop.width, Some(40.0));
}
#[test]
fn test_crop_is_full_image() {
let full = Crop::default();
assert!(full.is_full_image());
let full_explicit = Crop::with_dimensions(0.0, 0.0, 100.0, 100.0);
assert!(full_explicit.is_full_image());
let partial = Crop::with_dimensions(10.0, 10.0, 50.0, 50.0);
assert!(!partial.is_full_image());
}
#[test]
fn test_crop_is_valid() {
let valid = Crop::with_dimensions(10.0, 10.0, 50.0, 50.0);
assert!(valid.is_valid());
let invalid_overflow = Crop::with_dimensions(60.0, 60.0, 50.0, 50.0);
assert!(!invalid_overflow.is_valid());
let invalid_negative = Crop {
top: Some(-10.0),
..Default::default()
};
assert!(!invalid_negative.is_valid());
}
#[test]
fn test_non_event_for_event() {
let non_event = NonEvent::for_event("MARR");
assert_eq!(non_event.event_type, "MARR");
assert_eq!(non_event.event_description(), "Marriage");
}
#[test]
fn test_non_event_description() {
assert_eq!(NonEvent::for_event("MARR").event_description(), "Marriage");
assert_eq!(
NonEvent::for_event("CHR").event_description(),
"Christening"
);
assert_eq!(NonEvent::for_event("BAPM").event_description(), "Baptism");
assert_eq!(NonEvent::for_event("BURI").event_description(), "Burial");
assert_eq!(NonEvent::for_event("CUSTOM").event_description(), "CUSTOM");
}
#[test]
fn test_phrase_with_value() {
let phrase = Phrase::with_value("The Ides of March");
assert_eq!(phrase.value, "The Ides of March");
}
}