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)]
13pub struct LemmaSpecSet {
14 pub repository: Arc<LemmaRepository>,
15 pub name: String,
16 specs: BTreeMap<EffectiveDate, LemmaSpec>,
17}
18
19impl serde::Serialize for LemmaSpecSet {
20 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
21 where
22 S: serde::Serializer,
23 {
24 use serde::ser::SerializeStruct;
25 let mut state = serializer.serialize_struct("LemmaSpecSet", 3)?;
26 state.serialize_field("repository", &self.repository)?;
27 state.serialize_field("name", &self.name)?;
28 let specs: Vec<_> = self.iter_specs().collect();
29 state.serialize_field("specs", &specs)?;
30 state.end()
31 }
32}
33
34impl LemmaSpecSet {
35 #[must_use]
36 pub fn new(repository: Arc<LemmaRepository>, name: String) -> Self {
37 Self {
38 repository,
39 name,
40 specs: BTreeMap::new(),
41 }
42 }
43
44 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 self.specs.is_empty()
47 }
48
49 #[must_use]
51 pub fn get_exact(&self, effective_from: Option<&DateTimeValue>) -> Option<&LemmaSpec> {
52 let key = EffectiveDate::from_option(effective_from.cloned());
53 self.specs.get(&key)
54 }
55
56 pub fn insert(&mut self, spec: LemmaSpec) -> bool {
58 assert_eq!(
59 spec.name, self.name,
60 "BUG: spec name mismatch in LemmaSpecSet::insert"
61 );
62 let key = spec.effective_from.clone();
63 if self.specs.contains_key(&key) {
64 return false;
65 }
66 self.specs.insert(key, spec);
67 true
68 }
69
70 pub fn remove(&mut self, effective_from: Option<&DateTimeValue>) -> bool {
72 let key = EffectiveDate::from_option(effective_from.cloned());
73 self.specs.remove(&key).is_some()
74 }
75
76 pub fn iter_specs(&self) -> impl Iterator<Item = &LemmaSpec> + '_ {
77 self.specs.values()
78 }
79
80 pub fn iter_with_ranges(
90 &self,
91 ) -> impl Iterator<Item = (&LemmaSpec, Option<DateTimeValue>, Option<DateTimeValue>)> + '_ {
92 self.iter_specs().map(move |spec| {
93 let (effective_from, effective_to) = self.effective_range(spec);
94 (spec, effective_from, effective_to)
95 })
96 }
97
98 #[must_use]
101 pub fn spec_at(&self, effective: &EffectiveDate) -> Option<&LemmaSpec> {
102 self.specs
103 .range(..=effective.clone())
104 .next_back()
105 .map(|(_, spec)| spec)
106 }
107
108 pub fn effective_range(
113 &self,
114 spec: &LemmaSpec,
115 ) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
116 let from = spec.effective_from().cloned();
117 let key = spec.effective_from.clone();
118 let exact = self.specs.get_key_value(&key).unwrap_or_else(|| {
119 unreachable!(
120 "BUG: effective_range called with spec '{}' not in spec set",
121 spec.name
122 )
123 });
124 let to = self
125 .specs
126 .range((
127 std::ops::Bound::Excluded(exact.0),
128 std::ops::Bound::Unbounded,
129 ))
130 .next()
131 .and_then(|(_, next)| next.effective_from().cloned());
132 (from, to)
133 }
134
135 #[must_use]
137 pub fn effective_dates(&self, spec: &LemmaSpec, context: &Context) -> Vec<EffectiveDate> {
138 let (from, to) = self.effective_range(spec);
139 let from_key = EffectiveDate::from_option(from);
140 let all_dates: BTreeSet<EffectiveDate> =
141 context.iter().map(|s| s.effective_from.clone()).collect();
142 match to {
143 Some(dt) => all_dates
144 .range(from_key..EffectiveDate::DateTimeValue(dt))
145 .cloned()
146 .collect(),
147 None => all_dates.range(from_key..).cloned().collect(),
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::parsing::ast::LemmaSpec;
156
157 fn main_repository() -> Arc<LemmaRepository> {
158 Arc::new(LemmaRepository::new(None))
159 }
160
161 use crate::literals::DateGranularity;
162
163 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
164 DateTimeValue {
165 year,
166 month,
167 day,
168 hour: 0,
169 minute: 0,
170 second: 0,
171 microsecond: 0,
172 timezone: None,
173 granularity: DateGranularity::Full,
174 }
175 }
176
177 fn make_spec(name: &str) -> LemmaSpec {
178 LemmaSpec::new(name.to_string())
179 }
180
181 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
182 let mut spec = LemmaSpec::new(name.to_string());
183 spec.effective_from = EffectiveDate::from_option(effective_from);
184 spec
185 }
186
187 #[test]
192 #[should_panic(expected = "BUG")]
193 fn insert_panics_on_spec_name_mismatch() {
194 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
195 ss.insert(make_spec("b"));
196 }
197
198 #[test]
199 fn effective_range_unbounded_single_spec() {
200 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
201 let spec = make_spec("a");
202 assert!(ss.insert(spec));
203 let stored = ss.get_exact(None).expect("inserted");
204
205 let (from, to) = ss.effective_range(stored);
206 assert_eq!(from, None);
207 assert_eq!(to, None);
208 }
209
210 #[test]
211 fn effective_range_soft_end_from_next_spec() {
212 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
213 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
214 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
215
216 let v1 = ss.get_exact(Some(&date(2025, 1, 1))).expect("v1");
217 let (from, to) = ss.effective_range(v1);
218 assert_eq!(from, Some(date(2025, 1, 1)));
219 assert_eq!(to, Some(date(2025, 6, 1)));
220
221 let v2 = ss.get_exact(Some(&date(2025, 6, 1))).expect("v2");
222 let (from, to) = ss.effective_range(v2);
223 assert_eq!(from, Some(date(2025, 6, 1)));
224 assert_eq!(to, None);
225 }
226
227 #[test]
231 fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
232 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
233 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
234 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
235
236 let entries: Vec<_> = ss.iter_with_ranges().collect();
237 assert_eq!(entries.len(), 2);
238
239 let (spec_0, from_0, to_0) = &entries[0];
240 assert_eq!(spec_0.effective_from(), Some(&date(2025, 1, 1)));
241 assert_eq!(from_0, &Some(date(2025, 1, 1)));
242 assert_eq!(
243 to_0,
244 &Some(date(2025, 6, 1)),
245 "earlier row ends at the next row's effective_from"
246 );
247
248 let (spec_1, from_1, to_1) = &entries[1];
249 assert_eq!(spec_1.effective_from(), Some(&date(2025, 6, 1)));
250 assert_eq!(from_1, &Some(date(2025, 6, 1)));
251 assert_eq!(
252 to_1, &None,
253 "latest row has no successor; effective_to is None"
254 );
255 }
256
257 #[test]
258 fn effective_range_unbounded_start_with_successor() {
259 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
260 assert!(ss.insert(make_spec("a")));
261 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 3, 1)))));
262
263 let v1 = ss.get_exact(None).expect("v1");
264 let (from, to) = ss.effective_range(v1);
265 assert_eq!(from, None);
266 assert_eq!(to, Some(date(2025, 3, 1)));
267 }
268}