1use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct LemmaSpecSet {
13 pub repository: Arc<LemmaRepository>,
14 pub name: String,
15 specs: BTreeMap<EffectiveDate, LemmaSpec>,
16}
17
18impl LemmaSpecSet {
19 #[must_use]
20 pub fn new(repository: Arc<LemmaRepository>, name: String) -> Self {
21 Self {
22 repository,
23 name,
24 specs: BTreeMap::new(),
25 }
26 }
27
28 #[must_use]
29 pub fn is_empty(&self) -> bool {
30 self.specs.is_empty()
31 }
32
33 #[must_use]
35 pub fn get_exact(&self, effective_from: Option<&DateTimeValue>) -> Option<&LemmaSpec> {
36 let key = EffectiveDate::from_option(effective_from.cloned());
37 self.specs.get(&key)
38 }
39
40 pub fn insert(&mut self, spec: LemmaSpec) -> bool {
42 assert_eq!(
43 spec.name, self.name,
44 "BUG: spec name mismatch in LemmaSpecSet::insert"
45 );
46 let key = spec.effective_from.clone();
47 if self.specs.contains_key(&key) {
48 return false;
49 }
50 self.specs.insert(key, spec);
51 true
52 }
53
54 pub fn remove(&mut self, effective_from: Option<&DateTimeValue>) -> bool {
56 let key = EffectiveDate::from_option(effective_from.cloned());
57 self.specs.remove(&key).is_some()
58 }
59
60 pub fn iter_specs(&self) -> impl Iterator<Item = &LemmaSpec> + '_ {
61 self.specs.values()
62 }
63
64 pub fn iter_with_ranges(
74 &self,
75 ) -> impl Iterator<Item = (&LemmaSpec, Option<DateTimeValue>, Option<DateTimeValue>)> + '_ {
76 self.iter_specs().map(move |spec| {
77 let (effective_from, effective_to) = self.effective_range(spec);
78 (spec, effective_from, effective_to)
79 })
80 }
81
82 #[must_use]
85 pub fn spec_at(&self, effective: &EffectiveDate) -> Option<&LemmaSpec> {
86 self.specs
87 .range(..=effective.clone())
88 .next_back()
89 .map(|(_, spec)| spec)
90 }
91
92 pub fn effective_range(
97 &self,
98 spec: &LemmaSpec,
99 ) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
100 let from = spec.effective_from().cloned();
101 let key = spec.effective_from.clone();
102 let exact = self.specs.get_key_value(&key).unwrap_or_else(|| {
103 unreachable!(
104 "BUG: effective_range called with spec '{}' not in spec set",
105 spec.name
106 )
107 });
108 let to = self
109 .specs
110 .range((
111 std::ops::Bound::Excluded(exact.0),
112 std::ops::Bound::Unbounded,
113 ))
114 .next()
115 .and_then(|(_, next)| next.effective_from().cloned());
116 (from, to)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::parsing::ast::LemmaSpec;
124
125 fn main_repository() -> Arc<LemmaRepository> {
126 Arc::new(LemmaRepository::new(None))
127 }
128
129 use crate::literals::DateGranularity;
130
131 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
132 DateTimeValue {
133 year,
134 month,
135 day,
136 hour: 0,
137 minute: 0,
138 second: 0,
139 microsecond: 0,
140 timezone: None,
141 granularity: DateGranularity::Full,
142 }
143 }
144
145 fn make_spec(name: &str) -> LemmaSpec {
146 LemmaSpec::new(name.to_string())
147 }
148
149 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
150 let mut spec = LemmaSpec::new(name.to_string());
151 spec.effective_from = EffectiveDate::from_option(effective_from);
152 spec
153 }
154
155 #[test]
160 #[should_panic(expected = "BUG")]
161 fn insert_panics_on_spec_name_mismatch() {
162 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
163 ss.insert(make_spec("b"));
164 }
165
166 #[test]
167 fn effective_range_unbounded_single_spec() {
168 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
169 let spec = make_spec("a");
170 assert!(ss.insert(spec));
171 let stored = ss.get_exact(None).expect("inserted");
172
173 let (from, to) = ss.effective_range(stored);
174 assert_eq!(from, None);
175 assert_eq!(to, None);
176 }
177
178 #[test]
179 fn effective_range_soft_end_from_next_spec() {
180 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
181 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
182 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
183
184 let v1 = ss.get_exact(Some(&date(2025, 1, 1))).expect("v1");
185 let (from, to) = ss.effective_range(v1);
186 assert_eq!(from, Some(date(2025, 1, 1)));
187 assert_eq!(to, Some(date(2025, 6, 1)));
188
189 let v2 = ss.get_exact(Some(&date(2025, 6, 1))).expect("v2");
190 let (from, to) = ss.effective_range(v2);
191 assert_eq!(from, Some(date(2025, 6, 1)));
192 assert_eq!(to, None);
193 }
194
195 #[test]
199 fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
200 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
201 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
202 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
203
204 let entries: Vec<_> = ss.iter_with_ranges().collect();
205 assert_eq!(entries.len(), 2);
206
207 let (spec_0, from_0, to_0) = &entries[0];
208 assert_eq!(spec_0.effective_from(), Some(&date(2025, 1, 1)));
209 assert_eq!(from_0, &Some(date(2025, 1, 1)));
210 assert_eq!(
211 to_0,
212 &Some(date(2025, 6, 1)),
213 "earlier row ends at the next row's effective_from"
214 );
215
216 let (spec_1, from_1, to_1) = &entries[1];
217 assert_eq!(spec_1.effective_from(), Some(&date(2025, 6, 1)));
218 assert_eq!(from_1, &Some(date(2025, 6, 1)));
219 assert_eq!(
220 to_1, &None,
221 "latest row has no successor; effective_to is None"
222 );
223 }
224
225 #[test]
226 fn effective_range_unbounded_start_with_successor() {
227 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
228 assert!(ss.insert(make_spec("a")));
229 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 3, 1)))));
230
231 let v1 = ss.get_exact(None).expect("v1");
232 let (from, to) = ss.effective_range(v1);
233 assert_eq!(from, None);
234 assert_eq!(to, Some(date(2025, 3, 1)));
235 }
236}