#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
use crate::{parser::parse_subset, tokenizer::Tokenizer, GedcomError};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub enum Age {
Child,
Infant,
Stillborn,
Numeric {
years: Option<u16>,
months: Option<u8>,
weeks: Option<u8>,
days: Option<u8>,
modifier: AgeModifier,
phrase: Option<String>,
},
}
impl Age {
pub fn new(tokenizer: &mut Tokenizer<'_>, level: u8) -> Result<Age, GedcomError> {
let value = &tokenizer.take_line_value()?;
let mut age = match value.as_str() {
"CHILD" => Age::Child,
"INFANT" => Age::Infant,
"STILLBORN" => Age::Stillborn,
_ => {
let mut remaining: &str = value;
let modifier = if remaining.starts_with('<') {
remaining = remaining[1..].trim_start();
AgeModifier::LessThan
} else if remaining.starts_with('>') {
remaining = remaining[1..].trim_start();
AgeModifier::GreaterThan
} else {
AgeModifier::Exact
};
let mut years = None;
let mut months = None;
let mut weeks = None;
let mut days = None;
for token in remaining.split_whitespace() {
let (num_str, suffix) = token.split_at(token.len() - 1);
match suffix {
"y" => years = num_str.parse().ok(),
"m" => months = num_str.parse().ok(),
"w" => weeks = num_str.parse().ok(),
"d" => days = num_str.parse().ok(),
_ => {}
}
}
if years.is_none() && months.is_none() && weeks.is_none() && days.is_none() {
return Err(GedcomError::ParseError {
line: tokenizer.line,
message: format!("Invalid AGE value: {value}"),
});
}
Age::Numeric {
years,
months,
weeks,
days,
modifier,
phrase: None,
}
}
};
parse_subset(tokenizer, level, |tag, handler| {
if tag == "PHRASE" {
if let Age::Numeric { ref mut phrase, .. } = age {
*phrase = Some(handler.take_line_value()?);
}
}
Ok(())
})?;
Ok(age)
}
}
impl std::fmt::Display for Age {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Age::Child => write!(f, "CHILD"),
Age::Infant => write!(f, "INFANT"),
Age::Stillborn => write!(f, "STILLBORN"),
Age::Numeric {
years,
months,
weeks,
days,
modifier,
phrase: _,
} => {
match modifier {
AgeModifier::GreaterThan => write!(f, "> ")?,
AgeModifier::LessThan => write!(f, "< ")?,
AgeModifier::Exact => {}
}
let mut first = true;
if let Some(y) = years {
write!(f, "{y}y")?;
first = false;
}
if let Some(m) = months {
if !first {
write!(f, " ")?;
}
write!(f, "{m}m")?;
first = false;
}
if let Some(w) = weeks {
if !first {
write!(f, " ")?;
}
write!(f, "{w}w")?;
first = false;
}
if let Some(d) = days {
if !first {
write!(f, " ")?;
}
write!(f, "{d}d")?;
}
Ok(())
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
pub enum AgeModifier {
#[default]
Exact,
LessThan,
GreaterThan,
}
#[cfg(test)]
mod test {
use crate::{
types::age::{Age, AgeModifier},
Gedcom,
};
fn help_parse_age(age_value: &str) -> Age {
let sample = format!(
"0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 @I1@ INDI\n1 NAME Test /Person/\n1 DEAT Y\n2 AGE {age_value}\n0 TRLR"
);
let mut doc = Gedcom::new(sample.chars()).unwrap();
let data = doc.parse_data().unwrap();
data.individuals[0].events[0].age.clone().unwrap()
}
#[test]
fn test_parse_keyword_child() {
assert_eq!(help_parse_age("CHILD"), Age::Child);
}
#[test]
fn test_parse_keyword_infant() {
assert_eq!(help_parse_age("INFANT"), Age::Infant);
}
#[test]
fn test_parse_keyword_stillborn() {
assert_eq!(help_parse_age("STILLBORN"), Age::Stillborn);
}
#[test]
fn test_parse_numeric_years_months() {
assert_eq!(
help_parse_age("75y 3m"),
Age::Numeric {
years: Some(75),
months: Some(3),
weeks: None,
days: None,
modifier: AgeModifier::Exact,
phrase: None,
}
);
}
#[test]
fn test_parse_numeric_years_only() {
assert_eq!(
help_parse_age("25y"),
Age::Numeric {
years: Some(25),
months: None,
weeks: None,
days: None,
modifier: AgeModifier::Exact,
phrase: None,
}
);
}
#[test]
fn test_parse_modifier_greater_than() {
assert_eq!(
help_parse_age("> 80y"),
Age::Numeric {
years: Some(80),
months: None,
weeks: None,
days: None,
modifier: AgeModifier::GreaterThan,
phrase: None,
}
);
}
#[test]
fn test_parse_modifier_less_than() {
assert_eq!(
help_parse_age("< 6m"),
Age::Numeric {
years: None,
months: Some(6),
weeks: None,
days: None,
modifier: AgeModifier::LessThan,
phrase: None,
}
);
}
#[test]
fn test_parse_weeks_days() {
assert_eq!(
help_parse_age("2w 3d"),
Age::Numeric {
years: None,
months: None,
weeks: Some(2),
days: Some(3),
modifier: AgeModifier::Exact,
phrase: None,
}
);
}
#[test]
fn test_parse_phrase() {
let sample = "0 HEAD\n1 GEDC\n2 VERS 7.0\n0 @I1@ INDI\n1 NAME Test /Person/\n1 DEAT Y\n2 AGE 0y\n3 PHRASE STILLBORN\n0 TRLR";
let mut doc = Gedcom::new(sample.chars()).unwrap();
let data = doc.parse_data().unwrap();
let age = data.individuals[0].events[0].age.clone().unwrap();
assert_eq!(
age,
Age::Numeric {
years: Some(0),
months: None,
weeks: None,
days: None,
modifier: AgeModifier::Exact,
phrase: Some("STILLBORN".to_string()),
}
);
}
#[test]
fn test_display_roundtrip() {
let cases = ["CHILD", "INFANT", "STILLBORN", "75y 3m", "> 80y", "2w 3d"];
for input in cases {
let age = help_parse_age(input);
assert_eq!(age.to_string(), input);
}
}
}