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 use crate::literals::DateGranularity;
319
320 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
321 DateTimeValue {
322 year,
323 month,
324 day,
325 hour: 0,
326 minute: 0,
327 second: 0,
328 microsecond: 0,
329 timezone: None,
330 granularity: DateGranularity::Full,
331 }
332 }
333
334 fn make_spec(name: &str) -> LemmaSpec {
335 LemmaSpec::new(name.to_string())
336 }
337
338 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
339 let mut spec = LemmaSpec::new(name.to_string());
340 spec.effective_from = EffectiveDate::from_option(effective_from);
341 spec
342 }
343
344 #[test]
345 fn effective_range_unbounded_single_spec() {
346 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
347 let spec = Arc::new(make_spec("a"));
348 assert!(ss.insert(Arc::clone(&spec)));
349
350 let (from, to) = ss.effective_range(&spec);
351 assert_eq!(from, None);
352 assert_eq!(to, None);
353 }
354
355 #[test]
356 fn effective_range_soft_end_from_next_spec() {
357 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
358 let v1 = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
359 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))));
360 assert!(ss.insert(Arc::clone(&v1)));
361 assert!(ss.insert(Arc::clone(&v2)));
362
363 let (from, to) = ss.effective_range(&v1);
364 assert_eq!(from, Some(date(2025, 1, 1)));
365 assert_eq!(to, Some(date(2025, 6, 1)));
366
367 let (from, to) = ss.effective_range(&v2);
368 assert_eq!(from, Some(date(2025, 6, 1)));
369 assert_eq!(to, None);
370 }
371
372 #[test]
376 fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
377 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
378 let earlier = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
379 let latest = Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))));
380 assert!(ss.insert(Arc::clone(&earlier)));
381 assert!(ss.insert(Arc::clone(&latest)));
382
383 let entries: Vec<_> = ss.iter_with_ranges().collect();
384 assert_eq!(entries.len(), 2);
385
386 let (spec_0, from_0, to_0) = &entries[0];
387 assert!(Arc::ptr_eq(spec_0, &earlier));
388 assert_eq!(from_0, &Some(date(2025, 1, 1)));
389 assert_eq!(
390 to_0,
391 &Some(date(2025, 6, 1)),
392 "earlier row ends at the next row's effective_from"
393 );
394
395 let (spec_1, from_1, to_1) = &entries[1];
396 assert!(Arc::ptr_eq(spec_1, &latest));
397 assert_eq!(from_1, &Some(date(2025, 6, 1)));
398 assert_eq!(
399 to_1, &None,
400 "latest row has no successor; effective_to is None"
401 );
402 }
403
404 #[test]
405 fn effective_range_unbounded_start_with_successor() {
406 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
407 let v1 = Arc::new(make_spec("a"));
408 let v2 = Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1))));
409 assert!(ss.insert(Arc::clone(&v1)));
410 assert!(ss.insert(Arc::clone(&v2)));
411
412 let (from, to) = ss.effective_range(&v1);
413 assert_eq!(from, None);
414 assert_eq!(to, Some(date(2025, 3, 1)));
415 }
416
417 #[test]
418 fn temporal_boundaries_single_spec() {
419 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
420 assert!(ss.insert(Arc::new(make_spec("a"))));
421 assert!(ss.temporal_boundaries().is_empty());
422 }
423
424 #[test]
425 fn temporal_boundaries_multiple_specs() {
426 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
427 assert!(ss.insert(Arc::new(make_spec("a"))));
428 assert!(ss.insert(Arc::new(make_spec_with_range("a", Some(date(2025, 3, 1))))));
429 assert!(ss.insert(Arc::new(make_spec_with_range("a", Some(date(2025, 6, 1))))));
430
431 assert_eq!(
432 ss.temporal_boundaries(),
433 vec![date(2025, 3, 1), date(2025, 6, 1)]
434 );
435 }
436
437 #[test]
438 fn coverage_empty_set_is_full_gap() {
439 let ss = LemmaSpecSet::new(main_repository(), "missing".to_string());
440 let gaps = ss.coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 6, 1)));
441 assert_eq!(gaps, vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]);
442 }
443
444 #[test]
445 fn coverage_single_unbounded_spec_covers_everything() {
446 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
447 assert!(ss.insert(Arc::new(make_spec("dep"))));
448
449 assert!(ss.coverage_gaps(None, None).is_empty());
450 assert!(ss
451 .coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)))
452 .is_empty());
453 }
454
455 #[test]
456 fn coverage_single_spec_with_from_leaves_leading_gap() {
457 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
458 assert!(ss.insert(Arc::new(make_spec_with_range(
459 "dep",
460 Some(date(2025, 3, 1))
461 ))));
462
463 assert_eq!(
464 ss.coverage_gaps(None, None),
465 vec![(None, Some(date(2025, 3, 1)))]
466 );
467 }
468
469 #[test]
470 fn coverage_continuous_specs_no_gaps() {
471 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
472 assert!(ss.insert(Arc::new(make_spec_with_range(
473 "dep",
474 Some(date(2025, 1, 1))
475 ))));
476 assert!(ss.insert(Arc::new(make_spec_with_range(
477 "dep",
478 Some(date(2025, 6, 1))
479 ))));
480
481 assert!(ss
482 .coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1)))
483 .is_empty());
484 }
485
486 #[test]
487 fn coverage_dep_starts_after_required_start() {
488 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
489 assert!(ss.insert(Arc::new(make_spec_with_range(
490 "dep",
491 Some(date(2025, 6, 1))
492 ))));
493
494 assert_eq!(
495 ss.coverage_gaps(Some(&date(2025, 1, 1)), Some(&date(2025, 12, 1))),
496 vec![(Some(date(2025, 1, 1)), Some(date(2025, 6, 1)))]
497 );
498 }
499
500 #[test]
501 fn coverage_unbounded_required_range() {
502 let mut ss = LemmaSpecSet::new(main_repository(), "dep".to_string());
503 assert!(ss.insert(Arc::new(make_spec_with_range(
504 "dep",
505 Some(date(2025, 6, 1))
506 ))));
507
508 assert_eq!(
509 ss.coverage_gaps(None, None),
510 vec![(None, Some(date(2025, 6, 1)))]
511 );
512 }
513}