1use crate::engine::Context;
4use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, 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)]
86pub struct LemmaSpecSet {
87 pub repository: Arc<LemmaRepository>,
88 pub name: String,
89 specs: BTreeMap<EffectiveDate, Arc<LemmaSpec>>,
90}
91
92impl serde::Serialize for LemmaSpecSet {
93 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94 where
95 S: serde::Serializer,
96 {
97 use serde::ser::SerializeStruct;
98 let mut state = serializer.serialize_struct("LemmaSpecSet", 3)?;
99 state.serialize_field("repository", &self.repository)?;
100 state.serialize_field("name", &self.name)?;
101 let specs: Vec<_> = self.iter_specs().collect();
102 state.serialize_field("specs", &specs)?;
103 state.end()
104 }
105}
106
107impl LemmaSpecSet {
108 #[must_use]
109 pub fn new(repository: Arc<LemmaRepository>, name: String) -> Self {
110 Self {
111 repository,
112 name,
113 specs: BTreeMap::new(),
114 }
115 }
116
117 #[must_use]
118 pub fn is_empty(&self) -> bool {
119 self.specs.is_empty()
120 }
121
122 #[must_use]
123 pub fn len(&self) -> usize {
124 self.specs.len()
125 }
126
127 #[must_use]
128 pub fn first(&self) -> Option<&Arc<LemmaSpec>> {
129 self.specs.values().next()
130 }
131
132 #[must_use]
134 pub fn get_exact(&self, effective_from: Option<&DateTimeValue>) -> Option<&Arc<LemmaSpec>> {
135 let key = EffectiveDate::from_option(effective_from.cloned());
136 self.specs.get(&key)
137 }
138
139 pub fn insert(&mut self, spec: Arc<LemmaSpec>) -> bool {
141 debug_assert_eq!(spec.name, self.name);
142 let key = spec.effective_from.clone();
143 if self.specs.contains_key(&key) {
144 return false;
145 }
146 self.specs.insert(key, spec);
147 true
148 }
149
150 pub fn remove(&mut self, effective_from: Option<&DateTimeValue>) -> bool {
152 let key = EffectiveDate::from_option(effective_from.cloned());
153 self.specs.remove(&key).is_some()
154 }
155
156 pub fn iter_specs(&self) -> impl Iterator<Item = Arc<LemmaSpec>> + '_ {
157 self.specs.values().cloned()
158 }
159
160 pub fn iter_with_ranges(
170 &self,
171 ) -> impl Iterator<Item = (Arc<LemmaSpec>, Option<DateTimeValue>, Option<DateTimeValue>)> + '_
172 {
173 self.iter_specs().map(move |spec| {
174 let (effective_from, effective_to) = self.effective_range(&spec);
175 (spec, effective_from, effective_to)
176 })
177 }
178
179 pub fn specs_iter(&self) -> impl Iterator<Item = &Arc<LemmaSpec>> + '_ {
181 self.specs.values()
182 }
183
184 #[must_use]
187 pub fn spec_at(&self, effective: &EffectiveDate) -> Option<Arc<LemmaSpec>> {
188 self.specs
189 .range(..=effective.clone())
190 .next_back()
191 .map(|(_, spec)| Arc::clone(spec))
192 }
193
194 pub fn effective_range(
199 &self,
200 spec: &Arc<LemmaSpec>,
201 ) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
202 let from = spec.effective_from().cloned();
203 let key = spec.effective_from.clone();
204 let exact = self.specs.get_key_value(&key).unwrap_or_else(|| {
205 unreachable!(
206 "BUG: effective_range called with spec '{}' not in spec set",
207 spec.name
208 )
209 });
210 let to = self
211 .specs
212 .range((
213 std::ops::Bound::Excluded(exact.0),
214 std::ops::Bound::Unbounded,
215 ))
216 .next()
217 .and_then(|(_, next)| next.effective_from().cloned());
218 (from, to)
219 }
220
221 #[must_use]
223 pub fn temporal_boundaries(&self) -> Vec<DateTimeValue> {
224 self.specs
225 .values()
226 .filter_map(|s| s.effective_from().cloned())
227 .collect()
228 }
229
230 #[must_use]
232 pub fn effective_dates(&self, spec: &Arc<LemmaSpec>, context: &Context) -> Vec<EffectiveDate> {
233 let (from, to) = self.effective_range(spec);
234 let from_key = EffectiveDate::from_option(from);
235 let all_dates: BTreeSet<EffectiveDate> =
236 context.iter().map(|s| s.effective_from.clone()).collect();
237 match to {
238 Some(dt) => all_dates
239 .range(from_key..EffectiveDate::DateTimeValue(dt))
240 .cloned()
241 .collect(),
242 None => all_dates.range(from_key..).cloned().collect(),
243 }
244 }
245
246 #[must_use]
251 pub fn coverage_gaps(
252 &self,
253 required_from: Option<&DateTimeValue>,
254 required_to: Option<&DateTimeValue>,
255 ) -> Vec<(Option<DateTimeValue>, Option<DateTimeValue>)> {
256 let all_specs: Vec<&Arc<LemmaSpec>> = self.specs.values().collect();
257 if all_specs.is_empty() {
258 return vec![(required_from.cloned(), required_to.cloned())];
259 }
260
261 let req_start = TemporalBound::from_start(required_from);
262 let req_end = TemporalBound::from_end(required_to);
263
264 let intervals: Vec<(TemporalBound, TemporalBound)> = all_specs
265 .iter()
266 .enumerate()
267 .map(|(i, v)| {
268 let start = TemporalBound::from_start(v.effective_from());
269 let end = match all_specs.get(i + 1).and_then(|next| next.effective_from()) {
270 Some(next_from) => TemporalBound::At(next_from.clone()),
271 None => TemporalBound::PosInf,
272 };
273 (start, end)
274 })
275 .collect();
276
277 let mut gaps = Vec::new();
278 let mut cursor = req_start.clone();
279
280 for (v_start, v_end) in &intervals {
281 if cursor >= req_end {
282 break;
283 }
284
285 if *v_end <= cursor {
286 continue;
287 }
288
289 if *v_start > cursor {
290 let gap_end = std::cmp::min(v_start.clone(), req_end.clone());
291 if cursor < gap_end {
292 gaps.push((cursor.to_start(), gap_end.to_end()));
293 }
294 }
295
296 if *v_end > cursor {
297 cursor = v_end.clone();
298 }
299 }
300
301 if cursor < req_end {
302 gaps.push((cursor.to_start(), req_end.to_end()));
303 }
304
305 gaps
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::parsing::ast::LemmaSpec;
313
314 fn main_repository() -> Arc<LemmaRepository> {
315 Arc::new(LemmaRepository::new(None))
316 }
317
318 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
319 DateTimeValue {
320 year,
321 month,
322 day,
323 hour: 0,
324 minute: 0,
325 second: 0,
326 microsecond: 0,
327 timezone: None,
328 }
329 }
330
331 fn make_spec(name: &str) -> LemmaSpec {
332 LemmaSpec::new(name.to_string())
333 }
334
335 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
336 let mut spec = LemmaSpec::new(name.to_string());
337 spec.effective_from = EffectiveDate::from_option(effective_from);
338 spec
339 }
340
341 #[test]
342 fn effective_range_unbounded_single_spec() {
343 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
344 let spec = Arc::new(make_spec("a"));
345 assert!(ss.insert(Arc::clone(&spec)));
346
347 let (from, to) = ss.effective_range(&spec);
348 assert_eq!(from, None);
349 assert_eq!(to, None);
350 }
351
352 #[test]
353 fn effective_range_soft_end_from_next_spec() {
354 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
355 let v1 = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
356 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))));
357 assert!(ss.insert(Arc::clone(&v1)));
358 assert!(ss.insert(Arc::clone(&v2)));
359
360 let (from, to) = ss.effective_range(&v1);
361 assert_eq!(from, Some(date(2025, 1, 1)));
362 assert_eq!(to, Some(date(2025, 6, 1)));
363
364 let (from, to) = ss.effective_range(&v2);
365 assert_eq!(from, Some(date(2025, 6, 1)));
366 assert_eq!(to, None);
367 }
368
369 #[test]
373 fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
374 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
375 let earlier = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
376 let latest = Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))));
377 assert!(ss.insert(Arc::clone(&earlier)));
378 assert!(ss.insert(Arc::clone(&latest)));
379
380 let entries: Vec<_> = ss.iter_with_ranges().collect();
381 assert_eq!(entries.len(), 2);
382
383 let (spec_0, from_0, to_0) = &entries[0];
384 assert!(Arc::ptr_eq(spec_0, &earlier));
385 assert_eq!(from_0, &Some(date(2025, 1, 1)));
386 assert_eq!(
387 to_0,
388 &Some(date(2025, 6, 1)),
389 "earlier row ends at the next row's effective_from"
390 );
391
392 let (spec_1, from_1, to_1) = &entries[1];
393 assert!(Arc::ptr_eq(spec_1, &latest));
394 assert_eq!(from_1, &Some(date(2025, 6, 1)));
395 assert_eq!(
396 to_1, &None,
397 "latest row has no successor; effective_to is None"
398 );
399 }
400
401 #[test]
402 fn effective_range_unbounded_start_with_successor() {
403 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
404 let v1 = Arc::new(make_spec("a"));
405 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1))));
406 assert!(ss.insert(Arc::clone(&v1)));
407 assert!(ss.insert(Arc::clone(&v2)));
408
409 let (from, to) = ss.effective_range(&v1);
410 assert_eq!(from, None);
411 assert_eq!(to, Some(date(2025, 3, 1)));
412 }
413
414 #[test]
415 fn temporal_boundaries_single_spec() {
416 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
417 assert!(ss.insert(Arc::new(make_spec("a"))));
418 assert!(ss.temporal_boundaries().is_empty());
419 }
420
421 #[test]
422 fn temporal_boundaries_multiple_specs() {
423 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
424 assert!(ss.insert(Arc::new(make_spec("a"))));
425 assert!(ss.insert(Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1))))));
426 assert!(ss.insert(Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))))));
427
428 assert_eq!(
429 ss.temporal_boundaries(),
430 vec![date(2025, 3, 1), date(2025, 6, 1)]
431 );
432 }
433
434 #[test]
435 fn coverage_empty_set_is_full_gap() {
436 let ss = LemmaSpecSet::new(main_repository(), "missing".to_string());
437 let gaps = ss.coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 6, 1)));
438 assert_eq!(gaps, vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]);
439 }
440
441 #[test]
442 fn coverage_single_unbounded_spec_covers_everything() {
443 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
444 assert!(ss.insert(Arc::new(make_spec("dep"))));
445
446 assert!(ss.coverage_gaps(None, None).is_empty());
447 assert!(ss
448 .coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)))
449 .is_empty());
450 }
451
452 #[test]
453 fn coverage_single_spec_with_from_leaves_leading_gap() {
454 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
455 assert!(ss.insert(Arc::new(make_spec_with_range(
456 "dep",
457 Some(date(2025, 3, 1))
458 ))));
459
460 assert_eq!(
461 ss.coverage_gaps(None, None),
462 vec![(None, Some(date(2025, 3, 1)))]
463 );
464 }
465
466 #[test]
467 fn coverage_continuous_specs_no_gaps() {
468 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
469 assert!(ss.insert(Arc::new(make_spec_with_range(
470 "dep",
471 Some(date(2025, 1, 1))
472 ))));
473 assert!(ss.insert(Arc::new(make_spec_with_range(
474 "dep",
475 Some(date(2025, 6, 1))
476 ))));
477
478 assert!(ss
479 .coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)))
480 .is_empty());
481 }
482
483 #[test]
484 fn coverage_dep_starts_after_required_start() {
485 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
486 assert!(ss.insert(Arc::new(make_spec_with_range(
487 "dep",
488 Some(date(2025, 6, 1))
489 ))));
490
491 assert_eq!(
492 ss.coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1))),
493 vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]
494 );
495 }
496
497 #[test]
498 fn coverage_unbounded_required_range() {
499 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
500 assert!(ss.insert(Arc::new(make_spec_with_range(
501 "dep",
502 Some(date(2025, 6, 1))
503 ))));
504
505 assert_eq!(
506 ss.coverage_gaps(None, None),
507 vec![(None, Some(date(2025, 6, 1)))]
508 );
509 }
510}