1use crate::engine::Context;
4use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaSpec};
5use std::collections::{BTreeMap, BTreeSet};
6use std::sync::Arc;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
14pub(crate) enum TemporalBound {
15 NegInf,
16 At(DateTimeValue),
17 PosInf,
18}
19
20impl PartialOrd for TemporalBound {
21 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
22 Some(self.cmp(other))
23 }
24}
25
26impl Ord for TemporalBound {
27 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
28 use std::cmp::Ordering;
29 match (self, other) {
30 (TemporalBound::NegInf, TemporalBound::NegInf) => Ordering::Equal,
31 (TemporalBound::NegInf, _) => Ordering::Less,
32 (_, TemporalBound::NegInf) => Ordering::Greater,
33 (TemporalBound::PosInf, TemporalBound::PosInf) => Ordering::Equal,
34 (TemporalBound::PosInf, _) => Ordering::Greater,
35 (_, TemporalBound::PosInf) => Ordering::Less,
36 (TemporalBound::At(a), TemporalBound::At(b)) => a.cmp(b),
37 }
38 }
39}
40
41impl TemporalBound {
42 pub(crate) fn from_start(opt: Option<&DateTimeValue>) -> Self {
44 match opt {
45 None => TemporalBound::NegInf,
46 Some(d) => TemporalBound::At(d.clone()),
47 }
48 }
49
50 pub(crate) fn from_end(opt: Option<&DateTimeValue>) -> Self {
52 match opt {
53 None => TemporalBound::PosInf,
54 Some(d) => TemporalBound::At(d.clone()),
55 }
56 }
57
58 pub(crate) fn to_start(&self) -> Option<DateTimeValue> {
60 match self {
61 TemporalBound::NegInf => None,
62 TemporalBound::At(d) => Some(d.clone()),
63 TemporalBound::PosInf => {
64 unreachable!("BUG: PosInf cannot represent a start bound")
65 }
66 }
67 }
68
69 pub(crate) fn to_end(&self) -> Option<DateTimeValue> {
71 match self {
72 TemporalBound::NegInf => {
73 unreachable!("BUG: NegInf cannot represent an end bound")
74 }
75 TemporalBound::At(d) => Some(d.clone()),
76 TemporalBound::PosInf => None,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct LemmaSpecSet {
84 pub name: String,
85 specs: BTreeMap<EffectiveDate, Arc<LemmaSpec>>,
86}
87
88impl LemmaSpecSet {
89 #[must_use]
90 pub fn new(name: String) -> Self {
91 Self {
92 name,
93 specs: BTreeMap::new(),
94 }
95 }
96
97 #[must_use]
98 pub fn is_empty(&self) -> bool {
99 self.specs.is_empty()
100 }
101
102 #[must_use]
103 pub fn len(&self) -> usize {
104 self.specs.len()
105 }
106
107 #[must_use]
108 pub fn first(&self) -> Option<&Arc<LemmaSpec>> {
109 self.specs.values().next()
110 }
111
112 #[must_use]
114 pub fn get_exact(&self, effective_from: Option<&DateTimeValue>) -> Option<&Arc<LemmaSpec>> {
115 let key = EffectiveDate::from_option(effective_from.cloned());
116 self.specs.get(&key)
117 }
118
119 pub fn insert(&mut self, spec: Arc<LemmaSpec>) -> bool {
121 debug_assert_eq!(spec.name, self.name);
122 let key = spec.effective_from.clone();
123 if self.specs.contains_key(&key) {
124 return false;
125 }
126 self.specs.insert(key, spec);
127 true
128 }
129
130 pub fn remove(&mut self, effective_from: Option<&DateTimeValue>) -> bool {
132 let key = EffectiveDate::from_option(effective_from.cloned());
133 self.specs.remove(&key).is_some()
134 }
135
136 pub fn iter_specs(&self) -> impl Iterator<Item = Arc<LemmaSpec>> + '_ {
137 self.specs.values().cloned()
138 }
139
140 pub fn iter_with_ranges(
150 &self,
151 ) -> impl Iterator<Item = (Arc<LemmaSpec>, Option<DateTimeValue>, Option<DateTimeValue>)> + '_
152 {
153 self.iter_specs().map(move |spec| {
154 let (effective_from, effective_to) = self.effective_range(&spec);
155 (spec, effective_from, effective_to)
156 })
157 }
158
159 pub fn specs_iter(&self) -> impl Iterator<Item = &Arc<LemmaSpec>> + '_ {
161 self.specs.values()
162 }
163
164 #[must_use]
167 pub fn spec_at(&self, effective: &EffectiveDate) -> Option<Arc<LemmaSpec>> {
168 self.specs
169 .range(..=effective.clone())
170 .next_back()
171 .map(|(_, spec)| Arc::clone(spec))
172 }
173
174 pub fn effective_range(
179 &self,
180 spec: &Arc<LemmaSpec>,
181 ) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
182 let from = spec.effective_from().cloned();
183 let key = spec.effective_from.clone();
184 let exact = self.specs.get_key_value(&key).unwrap_or_else(|| {
185 unreachable!(
186 "BUG: effective_range called with spec '{}' not in spec set",
187 spec.name
188 )
189 });
190 let to = self
191 .specs
192 .range((
193 std::ops::Bound::Excluded(exact.0),
194 std::ops::Bound::Unbounded,
195 ))
196 .next()
197 .and_then(|(_, next)| next.effective_from().cloned());
198 (from, to)
199 }
200
201 #[must_use]
203 pub fn temporal_boundaries(&self) -> Vec<DateTimeValue> {
204 self.specs
205 .values()
206 .filter_map(|s| s.effective_from().cloned())
207 .collect()
208 }
209
210 #[must_use]
212 pub fn effective_dates(&self, spec: &Arc<LemmaSpec>, context: &Context) -> Vec<EffectiveDate> {
213 let (from, to) = self.effective_range(spec);
214 let from_key = EffectiveDate::from_option(from);
215 let all_dates: BTreeSet<EffectiveDate> =
216 context.iter().map(|s| s.effective_from.clone()).collect();
217 match to {
218 Some(dt) => all_dates
219 .range(from_key..EffectiveDate::DateTimeValue(dt))
220 .cloned()
221 .collect(),
222 None => all_dates.range(from_key..).cloned().collect(),
223 }
224 }
225
226 #[must_use]
231 pub fn coverage_gaps(
232 &self,
233 required_from: Option<&DateTimeValue>,
234 required_to: Option<&DateTimeValue>,
235 ) -> Vec<(Option<DateTimeValue>, Option<DateTimeValue>)> {
236 let all_specs: Vec<&Arc<LemmaSpec>> = self.specs.values().collect();
237 if all_specs.is_empty() {
238 return vec![(required_from.cloned(), required_to.cloned())];
239 }
240
241 let req_start = TemporalBound::from_start(required_from);
242 let req_end = TemporalBound::from_end(required_to);
243
244 let intervals: Vec<(TemporalBound, TemporalBound)> = all_specs
245 .iter()
246 .enumerate()
247 .map(|(i, v)| {
248 let start = TemporalBound::from_start(v.effective_from());
249 let end = match all_specs.get(i + 1).and_then(|next| next.effective_from()) {
250 Some(next_from) => TemporalBound::At(next_from.clone()),
251 None => TemporalBound::PosInf,
252 };
253 (start, end)
254 })
255 .collect();
256
257 let mut gaps = Vec::new();
258 let mut cursor = req_start.clone();
259
260 for (v_start, v_end) in &intervals {
261 if cursor >= req_end {
262 break;
263 }
264
265 if *v_end <= cursor {
266 continue;
267 }
268
269 if *v_start > cursor {
270 let gap_end = std::cmp::min(v_start.clone(), req_end.clone());
271 if cursor < gap_end {
272 gaps.push((cursor.to_start(), gap_end.to_end()));
273 }
274 }
275
276 if *v_end > cursor {
277 cursor = v_end.clone();
278 }
279 }
280
281 if cursor < req_end {
282 gaps.push((cursor.to_start(), req_end.to_end()));
283 }
284
285 gaps
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::parsing::ast::LemmaSpec;
293
294 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
295 DateTimeValue {
296 year,
297 month,
298 day,
299 hour: 0,
300 minute: 0,
301 second: 0,
302 microsecond: 0,
303 timezone: None,
304 }
305 }
306
307 fn make_spec(name: &str) -> LemmaSpec {
308 LemmaSpec::new(name.to_string())
309 }
310
311 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
312 let mut spec = LemmaSpec::new(name.to_string());
313 spec.effective_from = EffectiveDate::from_option(effective_from);
314 spec
315 }
316
317 #[test]
318 fn effective_range_unbounded_single_spec() {
319 let mut ss = LemmaSpecSet::new("a".to_string());
320 let spec = Arc::new(make_spec("a"));
321 assert!(ss.insert(Arc::clone(&spec)));
322
323 let (from, to) = ss.effective_range(&spec);
324 assert_eq!(from, None);
325 assert_eq!(to, None);
326 }
327
328 #[test]
329 fn effective_range_soft_end_from_next_spec() {
330 let mut ss = LemmaSpecSet::new("a".to_string());
331 let v1 = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
332 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))));
333 assert!(ss.insert(Arc::clone(&v1)));
334 assert!(ss.insert(Arc::clone(&v2)));
335
336 let (from, to) = ss.effective_range(&v1);
337 assert_eq!(from, Some(date(2025, 1, 1)));
338 assert_eq!(to, Some(date(2025, 6, 1)));
339
340 let (from, to) = ss.effective_range(&v2);
341 assert_eq!(from, Some(date(2025, 6, 1)));
342 assert_eq!(to, None);
343 }
344
345 #[test]
349 fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
350 let mut ss = LemmaSpecSet::new("a".to_string());
351 let earlier = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
352 let latest = Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))));
353 assert!(ss.insert(Arc::clone(&earlier)));
354 assert!(ss.insert(Arc::clone(&latest)));
355
356 let entries: Vec<_> = ss.iter_with_ranges().collect();
357 assert_eq!(entries.len(), 2);
358
359 let (spec_0, from_0, to_0) = &entries[0];
360 assert!(Arc::ptr_eq(spec_0, &earlier));
361 assert_eq!(from_0, &Some(date(2025, 1, 1)));
362 assert_eq!(
363 to_0,
364 &Some(date(2025, 6, 1)),
365 "earlier row ends at the next row's effective_from"
366 );
367
368 let (spec_1, from_1, to_1) = &entries[1];
369 assert!(Arc::ptr_eq(spec_1, &latest));
370 assert_eq!(from_1, &Some(date(2025, 6, 1)));
371 assert_eq!(
372 to_1, &None,
373 "latest row has no successor; effective_to is None"
374 );
375 }
376
377 #[test]
378 fn effective_range_unbounded_start_with_successor() {
379 let mut ss = LemmaSpecSet::new("a".to_string());
380 let v1 = Arc::new(make_spec("a"));
381 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1))));
382 assert!(ss.insert(Arc::clone(&v1)));
383 assert!(ss.insert(Arc::clone(&v2)));
384
385 let (from, to) = ss.effective_range(&v1);
386 assert_eq!(from, None);
387 assert_eq!(to, Some(date(2025, 3, 1)));
388 }
389
390 #[test]
391 fn temporal_boundaries_single_spec() {
392 let mut ss = LemmaSpecSet::new("a".to_string());
393 assert!(ss.insert(Arc::new(make_spec("a"))));
394 assert!(ss.temporal_boundaries().is_empty());
395 }
396
397 #[test]
398 fn temporal_boundaries_multiple_specs() {
399 let mut ss = LemmaSpecSet::new("a".to_string());
400 assert!(ss.insert(Arc::new(make_spec("a"))));
401 assert!(ss.insert(Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1))))));
402 assert!(ss.insert(Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))))));
403
404 assert_eq!(
405 ss.temporal_boundaries(),
406 vec![date(2025, 3, 1), date(2025, 6, 1)]
407 );
408 }
409
410 #[test]
411 fn coverage_empty_set_is_full_gap() {
412 let ss = LemmaSpecSet::new("missing".to_string());
413 let gaps = ss.coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 6, 1)));
414 assert_eq!(gaps, vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]);
415 }
416
417 #[test]
418 fn coverage_single_unbounded_spec_covers_everything() {
419 let mut ss = LemmaSpecSet::new("dep".to_string());
420 assert!(ss.insert(Arc::new(make_spec("dep"))));
421
422 assert!(ss.coverage_gaps(None, None).is_empty());
423 assert!(ss
424 .coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)))
425 .is_empty());
426 }
427
428 #[test]
429 fn coverage_single_spec_with_from_leaves_leading_gap() {
430 let mut ss = LemmaSpecSet::new("dep".to_string());
431 assert!(ss.insert(Arc::new(make_spec_with_range(
432 "dep",
433 Some(date(2025, 3, 1))
434 ))));
435
436 assert_eq!(
437 ss.coverage_gaps(None, None),
438 vec![(None, Some(date(2025, 3, 1)))]
439 );
440 }
441
442 #[test]
443 fn coverage_continuous_specs_no_gaps() {
444 let mut ss = LemmaSpecSet::new("dep".to_string());
445 assert!(ss.insert(Arc::new(make_spec_with_range(
446 "dep",
447 Some(date(2025, 1, 1))
448 ))));
449 assert!(ss.insert(Arc::new(make_spec_with_range(
450 "dep",
451 Some(date(2025, 6, 1))
452 ))));
453
454 assert!(ss
455 .coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)))
456 .is_empty());
457 }
458
459 #[test]
460 fn coverage_dep_starts_after_required_start() {
461 let mut ss = LemmaSpecSet::new("dep".to_string());
462 assert!(ss.insert(Arc::new(make_spec_with_range(
463 "dep",
464 Some(date(2025, 6, 1))
465 ))));
466
467 assert_eq!(
468 ss.coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1))),
469 vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]
470 );
471 }
472
473 #[test]
474 fn coverage_unbounded_required_range() {
475 let mut ss = LemmaSpecSet::new("dep".to_string());
476 assert!(ss.insert(Arc::new(make_spec_with_range(
477 "dep",
478 Some(date(2025, 6, 1))
479 ))));
480
481 assert_eq!(
482 ss.coverage_gaps(None, None),
483 vec![(None, Some(date(2025, 6, 1)))]
484 );
485 }
486}