#![no_std]
extern crate alloc;
use alloc::{string::String, vec::Vec};
use edtf_core::Edtf;
mod engine;
mod tables;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Language {
#[default]
English,
Russian,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumericOrder {
DayFirst,
MonthFirst,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Options {
pub language: Language,
pub numeric_order: Option<NumericOrder>,
pub default_century: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Note {
AlreadyValidEdtf,
CenturyPartInterval,
ModifierDropped,
CenturyMask,
BcCenturyInterval,
AstronomicalYear,
ElidedEndYear,
NumericUnambiguous,
NumericResolvedByOption,
NumericResolvedByLocale,
NumericOrderIrrelevant,
NumericOrderAmbiguous,
DecadeAmbiguity,
DefaultCenturyApplied,
SeasonCode,
OpenInterval,
MissingYearMasked,
QualifierDistributed,
OrAlternatives,
SeasonRangeCollision,
RomanCentury,
DecadeOfCentury,
EndpointYearDistributed,
CrossYearSeason,
}
impl Note {
#[must_use]
pub const fn decision(self) -> Option<&'static str> {
match self {
Self::AlreadyValidEdtf => None,
Self::CenturyPartInterval | Self::ModifierDropped => Some("N1"),
Self::CenturyMask | Self::BcCenturyInterval => Some("N2"),
Self::AstronomicalYear => Some("N3"),
Self::ElidedEndYear => Some("N4"),
Self::NumericUnambiguous
| Self::NumericResolvedByOption
| Self::NumericResolvedByLocale
| Self::NumericOrderIrrelevant
| Self::NumericOrderAmbiguous => Some("N5"),
Self::DecadeAmbiguity | Self::DefaultCenturyApplied | Self::DecadeOfCentury => {
Some("N6")
},
Self::SeasonCode => Some("N7"),
Self::OpenInterval => Some("N8"),
Self::MissingYearMasked => Some("N9"),
Self::QualifierDistributed => Some("N10"),
Self::SeasonRangeCollision => Some("N13"),
Self::OrAlternatives => Some("N14"),
Self::RomanCentury => Some("N15"),
Self::EndpointYearDistributed => Some("N16"),
Self::CrossYearSeason => Some("N17"),
}
}
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::AlreadyValidEdtf => "input was already valid EDTF; canonicalized",
Self::CenturyPartInterval => {
"part-of-century phrase mapped to a decade-rounded year interval"
},
Self::ModifierDropped => {
"early/mid/late modifier dropped (sub-decade precision would be false)"
},
Self::CenturyMask => "Nth century runs (N-1)01 to N00, so it masks as (N-1)XX",
Self::BcCenturyInterval => {
"BC centuries cannot be digit-masked; emitted as an exact year interval"
},
Self::AstronomicalYear => "BC year converted to astronomical numbering (year 0 exists)",
Self::ElidedEndYear => "elided end year inherits the start year's century",
Self::NumericUnambiguous => {
"field order provable from the input (year-first layout or a value over 12)"
},
Self::NumericResolvedByOption => "field order resolved by caller options",
Self::NumericResolvedByLocale => "field order implied by the language's convention",
Self::NumericOrderIrrelevant => "day and month are equal; order cannot matter",
Self::NumericOrderAmbiguous => "day/month order unknowable from the input",
Self::DecadeAmbiguity => "decade form has more than one plausible reading",
Self::DefaultCenturyApplied => "bare decade resolved by the configured century",
Self::SeasonCode => "season mapped to ISO 8601-2 sub-year grouping code",
Self::OpenInterval => "before/after expressed as an open interval",
Self::MissingYearMasked => "no year given; year masked as XXXX",
Self::QualifierDistributed => {
"whole-expression qualifier applied to every interval endpoint"
},
Self::OrAlternatives => "alternatives reported instead of picking one",
Self::SeasonRangeCollision => {
"NNNN-NN is both an EDTF sub-year code and a plausible year range"
},
Self::RomanCentury => {
"century read from a Roman numeral (Cyrillic lookalike letters tolerated)"
},
Self::DecadeOfCentury => "decade tied to the explicitly named century",
Self::EndpointYearDistributed => {
"endpoint without a year inherited the other endpoint's stated year"
},
Self::CrossYearSeason => {
"season-year-pair prose may name one boundary-spanning season or a range"
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Normalized {
pub edtf: String,
pub value: Edtf,
pub notes: Vec<Note>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Interpretation {
pub edtf: String,
pub value: Edtf,
pub reading: String,
pub notes: Vec<Note>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ambiguous {
pub interpretations: Vec<Interpretation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoMatchReason {
OutOfGrammar,
ExplicitNoDate,
ImpossibleDate,
}
impl NoMatchReason {
#[must_use]
pub const fn decision(self) -> &'static str {
match self {
Self::OutOfGrammar => "N11",
Self::ExplicitNoDate => "N12",
Self::ImpossibleDate => "N14",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Normalized(Normalized),
Ambiguous(Ambiguous),
NoMatch {
reason: NoMatchReason,
},
}
#[must_use]
pub fn normalize(input: &str) -> Outcome {
engine::run(input, Options::default())
}
#[must_use]
pub fn normalize_with(input: &str, options: Options) -> Outcome {
engine::run(input, options)
}