1use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone)]
14pub struct LemmaSpecSet {
15 pub repository: Arc<LemmaRepository>,
16 pub name: String,
17 specs: BTreeMap<EffectiveDate, Arc<LemmaSpec>>,
18}
19
20impl LemmaSpecSet {
21 #[must_use]
22 pub fn new(repository: Arc<LemmaRepository>, name: String) -> Self {
23 Self {
24 repository,
25 name,
26 specs: BTreeMap::new(),
27 }
28 }
29
30 #[must_use]
31 pub fn is_empty(&self) -> bool {
32 self.specs.is_empty()
33 }
34
35 #[must_use]
37 pub fn get_exact(&self, effective_from: Option<&DateTimeValue>) -> Option<&LemmaSpec> {
38 let key = EffectiveDate::from_option(effective_from.cloned());
39 self.specs.get(&key).map(AsRef::as_ref)
40 }
41
42 pub(crate) fn spec_arc(&self, spec: &LemmaSpec) -> &Arc<LemmaSpec> {
44 self.specs
45 .get(&spec.effective_from)
46 .expect("BUG: spec_arc called with a spec not in its spec set")
47 }
48
49 pub fn insert(&mut self, spec: LemmaSpec) -> bool {
51 self.insert_replacing(spec, None)
52 }
53
54 pub(crate) fn insert_replacing(
59 &mut self,
60 spec: LemmaSpec,
61 replaced: Option<Arc<LemmaSpec>>,
62 ) -> bool {
63 assert_eq!(
64 spec.name, self.name,
65 "BUG: spec name mismatch in LemmaSpecSet::insert"
66 );
67 let key = spec.effective_from.clone();
68 if self.specs.contains_key(&key) {
69 return false;
70 }
71 let spec_arc = match replaced {
72 Some(replaced) if *replaced == spec => replaced,
73 Some(_) | None => Arc::new(spec),
74 };
75 self.specs.insert(key, spec_arc);
76 true
77 }
78
79 pub(crate) fn insert_owned(&mut self, spec: Arc<LemmaSpec>) -> bool {
82 assert_eq!(
83 spec.name, self.name,
84 "BUG: spec name mismatch in LemmaSpecSet::insert"
85 );
86 self.specs
87 .insert(spec.effective_from.clone(), spec)
88 .is_none()
89 }
90
91 pub fn remove(&mut self, effective_from: Option<&DateTimeValue>) -> bool {
93 self.take(effective_from).is_some()
94 }
95
96 pub(crate) fn take(
98 &mut self,
99 effective_from: Option<&DateTimeValue>,
100 ) -> Option<Arc<LemmaSpec>> {
101 let key = EffectiveDate::from_option(effective_from.cloned());
102 self.specs.remove(&key)
103 }
104
105 pub fn iter_specs(&self) -> impl Iterator<Item = &LemmaSpec> + '_ {
106 self.specs.values().map(AsRef::as_ref)
107 }
108
109 pub fn iter_with_ranges(
119 &self,
120 ) -> impl Iterator<Item = (&LemmaSpec, Option<DateTimeValue>, Option<DateTimeValue>)> + '_ {
121 self.iter_specs().map(move |spec| {
122 let (effective_from, effective_to) = self.effective_range(spec);
123 (spec, effective_from, effective_to)
124 })
125 }
126
127 #[must_use]
130 pub fn spec_at(&self, effective: &EffectiveDate) -> Option<&LemmaSpec> {
131 self.specs
132 .range(..=effective.clone())
133 .next_back()
134 .map(|(_, spec)| spec.as_ref())
135 }
136
137 pub fn effective_range(
142 &self,
143 spec: &LemmaSpec,
144 ) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
145 let from = spec.effective_from().cloned();
146 let key = spec.effective_from.clone();
147 let exact = self.specs.get_key_value(&key).unwrap_or_else(|| {
148 unreachable!(
149 "BUG: effective_range called with spec '{}' not in spec set",
150 spec.name
151 )
152 });
153 let to = self
154 .specs
155 .range((
156 std::ops::Bound::Excluded(exact.0),
157 std::ops::Bound::Unbounded,
158 ))
159 .next()
160 .and_then(|(_, next)| next.effective_from().cloned());
161 (from, to)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::parsing::ast::LemmaSpec;
169
170 fn main_repository() -> Arc<LemmaRepository> {
171 Arc::new(LemmaRepository::new(None))
172 }
173
174 use crate::literals::DateGranularity;
175
176 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
177 DateTimeValue {
178 year,
179 month,
180 day,
181 hour: 0,
182 minute: 0,
183 second: 0,
184 microsecond: 0,
185 timezone: None,
186 granularity: DateGranularity::Full,
187 }
188 }
189
190 fn make_spec(name: &str) -> LemmaSpec {
191 LemmaSpec::new(name.to_string())
192 }
193
194 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
195 let mut spec = LemmaSpec::new(name.to_string());
196 spec.effective_from = EffectiveDate::from_option(effective_from);
197 spec
198 }
199
200 #[test]
205 #[should_panic(expected = "BUG")]
206 fn insert_panics_on_spec_name_mismatch() {
207 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
208 ss.insert(make_spec("b"));
209 }
210
211 #[test]
212 fn effective_range_unbounded_single_spec() {
213 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
214 let spec = make_spec("a");
215 assert!(ss.insert(spec));
216 let stored = ss.get_exact(None).expect("inserted");
217
218 let (from, to) = ss.effective_range(stored);
219 assert_eq!(from, None);
220 assert_eq!(to, None);
221 }
222
223 #[test]
224 fn effective_range_soft_end_from_next_spec() {
225 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
226 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
227 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
228
229 let v1 = ss.get_exact(Some(&date(2025, 1, 1))).expect("v1");
230 let (from, to) = ss.effective_range(v1);
231 assert_eq!(from, Some(date(2025, 1, 1)));
232 assert_eq!(to, Some(date(2025, 6, 1)));
233
234 let v2 = ss.get_exact(Some(&date(2025, 6, 1))).expect("v2");
235 let (from, to) = ss.effective_range(v2);
236 assert_eq!(from, Some(date(2025, 6, 1)));
237 assert_eq!(to, None);
238 }
239
240 #[test]
244 fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
245 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
246 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
247 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
248
249 let entries: Vec<_> = ss.iter_with_ranges().collect();
250 assert_eq!(entries.len(), 2);
251
252 let (spec_0, from_0, to_0) = &entries[0];
253 assert_eq!(spec_0.effective_from(), Some(&date(2025, 1, 1)));
254 assert_eq!(from_0, &Some(date(2025, 1, 1)));
255 assert_eq!(
256 to_0,
257 &Some(date(2025, 6, 1)),
258 "earlier row ends at the next row's effective_from"
259 );
260
261 let (spec_1, from_1, to_1) = &entries[1];
262 assert_eq!(spec_1.effective_from(), Some(&date(2025, 6, 1)));
263 assert_eq!(from_1, &Some(date(2025, 6, 1)));
264 assert_eq!(
265 to_1, &None,
266 "latest row has no successor; effective_to is None"
267 );
268 }
269
270 #[test]
271 fn effective_range_unbounded_start_with_successor() {
272 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
273 assert!(ss.insert(make_spec("a")));
274 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 3, 1)))));
275
276 let v1 = ss.get_exact(None).expect("v1");
277 let (from, to) = ss.effective_range(v1);
278 assert_eq!(from, None);
279 assert_eq!(to, Some(date(2025, 3, 1)));
280 }
281}