use crate::engine::Context;
use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct LemmaSpecSet {
pub repository: Arc<LemmaRepository>,
pub name: String,
specs: BTreeMap<EffectiveDate, LemmaSpec>,
}
impl serde::Serialize for LemmaSpecSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("LemmaSpecSet", 3)?;
state.serialize_field("repository", &self.repository)?;
state.serialize_field("name", &self.name)?;
let specs: Vec<_> = self.iter_specs().collect();
state.serialize_field("specs", &specs)?;
state.end()
}
}
impl LemmaSpecSet {
#[must_use]
pub fn new(repository: Arc<LemmaRepository>, name: String) -> Self {
Self {
repository,
name,
specs: BTreeMap::new(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.specs.is_empty()
}
#[must_use]
pub fn get_exact(&self, effective_from: Option<&DateTimeValue>) -> Option<&LemmaSpec> {
let key = EffectiveDate::from_option(effective_from.cloned());
self.specs.get(&key)
}
pub fn insert(&mut self, spec: LemmaSpec) -> bool {
assert_eq!(
spec.name, self.name,
"BUG: spec name mismatch in LemmaSpecSet::insert"
);
let key = spec.effective_from.clone();
if self.specs.contains_key(&key) {
return false;
}
self.specs.insert(key, spec);
true
}
pub fn remove(&mut self, effective_from: Option<&DateTimeValue>) -> bool {
let key = EffectiveDate::from_option(effective_from.cloned());
self.specs.remove(&key).is_some()
}
pub fn iter_specs(&self) -> impl Iterator<Item = &LemmaSpec> + '_ {
self.specs.values()
}
pub fn iter_with_ranges(
&self,
) -> impl Iterator<Item = (&LemmaSpec, Option<DateTimeValue>, Option<DateTimeValue>)> + '_ {
self.iter_specs().map(move |spec| {
let (effective_from, effective_to) = self.effective_range(spec);
(spec, effective_from, effective_to)
})
}
#[must_use]
pub fn spec_at(&self, effective: &EffectiveDate) -> Option<&LemmaSpec> {
self.specs
.range(..=effective.clone())
.next_back()
.map(|(_, spec)| spec)
}
pub fn effective_range(
&self,
spec: &LemmaSpec,
) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
let from = spec.effective_from().cloned();
let key = spec.effective_from.clone();
let exact = self.specs.get_key_value(&key).unwrap_or_else(|| {
unreachable!(
"BUG: effective_range called with spec '{}' not in spec set",
spec.name
)
});
let to = self
.specs
.range((
std::ops::Bound::Excluded(exact.0),
std::ops::Bound::Unbounded,
))
.next()
.and_then(|(_, next)| next.effective_from().cloned());
(from, to)
}
#[must_use]
pub fn effective_dates(&self, spec: &LemmaSpec, context: &Context) -> Vec<EffectiveDate> {
let (from, to) = self.effective_range(spec);
let from_key = EffectiveDate::from_option(from);
let all_dates: BTreeSet<EffectiveDate> =
context.iter().map(|s| s.effective_from.clone()).collect();
match to {
Some(dt) => all_dates
.range(from_key..EffectiveDate::DateTimeValue(dt))
.cloned()
.collect(),
None => all_dates.range(from_key..).cloned().collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parsing::ast::LemmaSpec;
fn main_repository() -> Arc<LemmaRepository> {
Arc::new(LemmaRepository::new(None))
}
use crate::literals::DateGranularity;
fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
DateTimeValue {
year,
month,
day,
hour: 0,
minute: 0,
second: 0,
microsecond: 0,
timezone: None,
granularity: DateGranularity::Full,
}
}
fn make_spec(name: &str) -> LemmaSpec {
LemmaSpec::new(name.to_string())
}
fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
let mut spec = LemmaSpec::new(name.to_string());
spec.effective_from = EffectiveDate::from_option(effective_from);
spec
}
#[test]
#[should_panic(expected = "BUG")]
fn insert_panics_on_spec_name_mismatch() {
let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
ss.insert(make_spec("b"));
}
#[test]
fn effective_range_unbounded_single_spec() {
let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
let spec = make_spec("a");
assert!(ss.insert(spec));
let stored = ss.get_exact(None).expect("inserted");
let (from, to) = ss.effective_range(stored);
assert_eq!(from, None);
assert_eq!(to, None);
}
#[test]
fn effective_range_soft_end_from_next_spec() {
let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
let v1 = ss.get_exact(Some(&date(2025, 1, 1))).expect("v1");
let (from, to) = ss.effective_range(v1);
assert_eq!(from, Some(date(2025, 1, 1)));
assert_eq!(to, Some(date(2025, 6, 1)));
let v2 = ss.get_exact(Some(&date(2025, 6, 1))).expect("v2");
let (from, to) = ss.effective_range(v2);
assert_eq!(from, Some(date(2025, 6, 1)));
assert_eq!(to, None);
}
#[test]
fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
let entries: Vec<_> = ss.iter_with_ranges().collect();
assert_eq!(entries.len(), 2);
let (spec_0, from_0, to_0) = &entries[0];
assert_eq!(spec_0.effective_from(), Some(&date(2025, 1, 1)));
assert_eq!(from_0, &Some(date(2025, 1, 1)));
assert_eq!(
to_0,
&Some(date(2025, 6, 1)),
"earlier row ends at the next row's effective_from"
);
let (spec_1, from_1, to_1) = &entries[1];
assert_eq!(spec_1.effective_from(), Some(&date(2025, 6, 1)));
assert_eq!(from_1, &Some(date(2025, 6, 1)));
assert_eq!(
to_1, &None,
"latest row has no successor; effective_to is None"
);
}
#[test]
fn effective_range_unbounded_start_with_successor() {
let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
assert!(ss.insert(make_spec("a")));
assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 3, 1)))));
let v1 = ss.get_exact(None).expect("v1");
let (from, to) = ss.effective_range(v1);
assert_eq!(from, None);
assert_eq!(to, Some(date(2025, 3, 1)));
}
}