#![allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test/bench code: a panic here is the failure signal, not a crash path"
)]
use edtf_core::Edtf;
use edtf_normalize::{Note, NumericOrder, Options, Outcome, normalize, normalize_with};
#[track_caller]
fn ok(input: &str, expected: &str, level: u8) {
match normalize(input) {
Outcome::Normalized(n) => {
assert_eq!(n.edtf, expected, "input: {input:?}");
let parsed = Edtf::parse(&n.edtf).expect("output must parse in core");
assert_eq!(parsed, n.value, "value/edtf mismatch for {input:?}");
assert_eq!(parsed.level(), level, "level mismatch for {input:?}");
},
other => panic!("expected Normalized for {input:?}, got {other:?}"),
}
}
#[track_caller]
fn ambiguous(input: &str, expected: &[&str]) {
match normalize(input) {
Outcome::Ambiguous(a) => {
let got: Vec<&str> = a.interpretations.iter().map(|i| i.edtf.as_str()).collect();
assert_eq!(got, expected, "input: {input:?}");
for i in &a.interpretations {
assert_eq!(
Edtf::parse(&i.edtf).expect("interpretation must parse"),
i.value
);
}
},
other => panic!("expected Ambiguous for {input:?}, got {other:?}"),
}
}
#[track_caller]
fn no_match(input: &str) {
assert!(
matches!(normalize(input), Outcome::NoMatch { .. }),
"expected NoMatch for {input:?}"
);
}
#[test]
fn bc_years_are_astronomical() {
ok("500 BC", "-0499", 1);
ok("500 BCE", "-0499", 1);
ok("c64 BCE", "-0063~", 1);
ok("1 AD", "0001", 0);
ok("79 CE", "0079", 0);
}
#[test]
fn centuries_are_off_by_one() {
ok("19th century", "18XX", 1);
ok("1st century", "00XX", 1);
ok("nineteenth century", "18XX", 1);
ok("10c", "09XX", 1);
ok("2nd century AD", "01XX", 1);
}
#[test]
fn bc_centuries_are_exact_intervals() {
ok("2nd century BC", "-0199/-0100", 1);
ok("2nd century BCE", "-0199/-0100", 1);
}
#[test]
fn century_parts_are_decade_rounded_intervals() {
ok("early 19th century", "1801/1830", 0);
ok("mid 19th century", "1831/1870", 0);
ok("late 19th century", "1871/1900", 0);
ok("first half of the 19th century", "1801/1850", 0);
ok("second half of the 19th century", "1851/1900", 0);
ok("late 2nd century BC", "-0129/-0100", 1);
ok("early 2nd century BC", "-0199/-0170", 1);
}
#[test]
fn first_century_bc_has_no_mask() {
ok("1st century BC", "-0099/0000", 1);
}
#[test]
fn decades() {
ok("1980s", "198X", 1);
ok("1980's", "198X", 1);
}
#[test]
fn bare_decades_are_ambiguous() {
ambiguous("the 80s", &["188X", "198X"]);
ambiguous("'80s", &["188X", "198X"]);
let opts = Options {
default_century: Some(1900),
..Options::default()
};
match normalize_with("the 80s", opts) {
Outcome::Normalized(n) => {
assert_eq!(n.edtf, "198X");
assert!(n.notes.contains(&Note::DefaultCenturyApplied));
},
other => panic!("expected Normalized, got {other:?}"),
}
}
#[test]
fn century_boundary_decades_are_ambiguous() {
ambiguous("1900s", &["190X", "19XX"]);
ambiguous("1800s", &["180X", "18XX"]);
}
#[test]
fn qualifiers() {
ok("circa 1920", "1920~", 1);
ok("ca. 1920", "1920~", 1);
ok("c.1920", "1920~", 1);
ok("c1920", "1920~", 1);
ok("about 1920", "1920~", 1);
ok("possibly 1920", "1920?", 1);
ok("probably 1920", "1920?", 1);
ok("circa 1840s", "184X~", 1);
ok("possibly circa 1920", "1920%", 1);
ok("19th century?", "18XX?", 1);
ok("ca. 19c", "18XX~", 1);
}
#[test]
fn seasons() {
ok("spring 2001", "2001-21", 1);
ok("Summer 1872", "1872-22", 1);
ok("Autumn 1872", "1872-23", 1);
ok("Fall 1872", "1872-23", 1);
ok("Winter 1872", "1872-24", 1);
ok("about Spring 1849", "1849-21~", 1);
}
#[test]
fn ranges() {
ok("1914-1918", "1914/1918", 0);
ok("1914\u{2013}18", "1914/1918", 0); ok("from 1914 to 1918", "1914/1918", 0);
ok("between 1914 and 1918", "1914/1918", 0);
ok("1851-52", "1851/1852", 0);
ok("1852 - 1860", "1852/1860", 0);
ok("1860s-1870s", "186X/187X", 2);
ok("1856-ca. 1865", "1856/1865~", 1);
ok("1857-mid 1860s", "1857/186X", 2); ok("17-19th centuries", "16XX/18XX", 2);
}
#[test]
fn reversed_ranges_are_rejected() {
no_match("1918-1914");
}
#[test]
fn before_after_are_open_intervals() {
ok("before 1917", "../1917", 1);
ok("after 1917", "1917/..", 1);
ok("until 1917", "../1917", 1);
ok("since 1917", "1917/..", 1);
ok("earlier than 1917", "../1917", 1);
ok("later than 1917", "1917/..", 1);
ok("before January 1928", "../1928-01", 1);
ok("before approx January 18 1928", "../1928-01-18~", 1);
ok("after about the 1920s", "192X~/..", 2);
ok("before 19th century", "../18XX", 2);
}
#[test]
fn month_and_day_forms() {
ok("June 1940", "1940-06", 0);
ok("Jun 1940", "1940-06", 0);
ok("Jun. 1940", "1940-06", 0);
ok("12 April 1985", "1985-04-12", 0);
ok("April 12, 1985", "1985-04-12", 0);
ok("12th April 1985", "1985-04-12", 0);
ok("the year 1800", "1800", 0);
}
#[test]
fn missing_year_is_masked() {
ok("January", "XXXX-01", 2);
ok("January 12", "XXXX-01-12", 2);
}
#[test]
fn numeric_dates_report_ambiguity() {
ambiguous("12/04/1985", &["1985-04-12", "1985-12-04"]);
let day_first = Options {
numeric_order: Some(NumericOrder::DayFirst),
..Options::default()
};
match normalize_with("12/04/1985", day_first) {
Outcome::Normalized(n) => {
assert_eq!(n.edtf, "1985-04-12");
assert!(n.notes.contains(&Note::NumericResolvedByOption));
},
other => panic!("expected Normalized, got {other:?}"),
}
ok("13/04/1985", "1985-04-13", 0);
ok("04/13/1985", "1985-04-13", 0);
ok("1985/04/12", "1985-04-12", 0);
ok("04/04/1985", "1985-04-04", 0);
ok("7/2008", "2008-07", 0);
no_match("12/04/85");
}
#[test]
fn impossible_numeric_dates_fail_closed() {
no_match("31/02/1985");
no_match("30/02/1985");
no_match("02/29/1985");
ok("02/29/1984", "1984-02-29", 0);
}
#[test]
fn season_code_range_collision() {
ambiguous("1914-21", &["1914-21", "1914/1921"]);
ambiguous("1910-30", &["1910-30", "1910/1930"]);
ok("1914-18", "1914/1918", 0);
ok("1861-67", "1861/1867", 0);
ok("1869-70", "1869/1870", 0);
}
#[test]
fn or_alternatives() {
ambiguous("1863 or 1864", &["1863", "1864"]);
}
#[test]
fn unknown_is_no_match() {
no_match("unknown");
no_match("undated");
no_match("n.d.");
no_match("");
no_match(" ");
}
#[test]
fn no_substring_extraction() {
no_match("this isn't a date");
no_match("23rd Dynasty");
no_match("1851-1852; printed 1853-1854");
no_match("1863, printed 1870");
no_match("notcirca 1860");
no_match("attica 1802");
no_match("birthday in 1872");
no_match("90");
}
#[test]
fn valid_edtf_passes_through() {
ok("1985-04-12", "1985-04-12", 0);
ok("198X", "198X", 1);
ok("2004-06~-11", "2004-06~-11", 2);
ok("1858/1860", "1858/1860", 0); match normalize("1860?") {
Outcome::Normalized(n) => assert!(n.notes.contains(&Note::AlreadyValidEdtf)),
other => panic!("expected Normalized, got {other:?}"),
}
}
#[test]
fn whole_expression_qualifier_distributes() {
match normalize("circa 1914-1918") {
Outcome::Normalized(n) => {
assert_eq!(n.edtf, "1914~/1918~");
assert!(n.notes.contains(&Note::QualifierDistributed));
},
other => panic!("expected Normalized, got {other:?}"),
}
ok("1868-1871?", "1868?/1871?", 1);
}
#[test]
fn negated_open_phrases() {
ok("no later than 1917", "../1917", 1);
ok("no earlier than 1917", "1917/..", 1);
}
#[test]
fn range_endpoints_inherit_the_stated_year() {
ok("June-July 1940", "1940-06/1940-07", 0);
ok("June to July 1940", "1940-06/1940-07", 0);
ok("between May and June 1940", "1940-05/1940-06", 0);
ok("12 January to 15 March 1940", "1940-01-12/1940-03-15", 0);
ok("from January 1940 to June", "1940-01/1940-06", 0);
no_match("July-June 1940");
}
#[test]
fn cross_year_winter_is_ambiguous() {
ambiguous("winter 1941-42", &["1941-24", "1941-24/1942"]);
ambiguous("winter 1941-1942", &["1941-24", "1941-24/1942"]);
ok("winter 1941-45", "1941-24/1945", 1);
}
#[test]
fn bc_starts_refuse_elided_end_years() {
no_match("500 BC to 18");
no_match("100 BC-18");
no_match("1900 BC to 5");
ok("500 BC to 418 BC", "-0499/-0417", 1);
}
#[test]
fn spaced_hyphens_mirror_the_unspaced_limits() {
no_match("2000 - 05"); ambiguous("1914 - 21", &["1914-21", "1914/1921"]);
ok("1914 - 18", "1914/1918", 0);
}
#[test]
fn dead_readings_poison_the_whole_input() {
no_match("31 June 1940 or 1 July 1940");
no_match("29 February 1900 or 1901");
no_match("1910s to 1900s");
no_match("1990s-1980s");
ambiguous("1 July 1940 or 2 July 1940", &["1940-07-01", "1940-07-02"]);
}
#[test]
fn out_of_domain_default_century_is_ignored() {
let opts = Options {
default_century: Some(10000),
..Options::default()
};
assert!(matches!(
normalize_with("the 80s", opts),
Outcome::Ambiguous(_)
));
}
#[test]
fn no_match_reasons_discriminate() {
use edtf_normalize::NoMatchReason;
assert!(matches!(
normalize("unknown"),
Outcome::NoMatch {
reason: NoMatchReason::ExplicitNoDate
}
));
assert!(matches!(
normalize("birthday in 1872"),
Outcome::NoMatch {
reason: NoMatchReason::OutOfGrammar
}
));
assert!(matches!(
normalize("30/02/1985"),
Outcome::NoMatch {
reason: NoMatchReason::ImpossibleDate
}
));
}
#[test]
fn notes_cite_decisions() {
let Outcome::Normalized(n) = normalize("500 BC") else {
panic!("expected Normalized");
};
assert!(
n.notes
.iter()
.any(|note| note.decision() == Some("N3") && !note.message().is_empty())
);
}