1use crate::parsing::ast::{DateTimeValue, EffectiveDate, LemmaRepository, LemmaSpec};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct LemmaSpecSet {
13 #[serde(with = "crate::snapshot::shared::arc")]
14 pub repository: Arc<LemmaRepository>,
15 pub name: String,
16 specs: BTreeMap<EffectiveDate, LemmaSpec>,
17}
18
19impl LemmaSpecSet {
20 #[must_use]
21 pub fn new(repository: Arc<LemmaRepository>, name: String) -> Self {
22 Self {
23 repository,
24 name,
25 specs: BTreeMap::new(),
26 }
27 }
28
29 #[must_use]
30 pub fn is_empty(&self) -> bool {
31 self.specs.is_empty()
32 }
33
34 #[must_use]
36 pub fn get_exact(&self, effective_from: Option<&DateTimeValue>) -> Option<&LemmaSpec> {
37 let key = EffectiveDate::from_option(effective_from.cloned());
38 self.specs.get(&key)
39 }
40
41 pub fn insert(&mut self, spec: LemmaSpec) -> bool {
43 assert_eq!(
44 spec.name, self.name,
45 "BUG: spec name mismatch in LemmaSpecSet::insert"
46 );
47 let key = spec.effective_from.clone();
48 if self.specs.contains_key(&key) {
49 return false;
50 }
51 self.specs.insert(key, spec);
52 true
53 }
54
55 pub fn remove(&mut self, effective_from: Option<&DateTimeValue>) -> bool {
57 let key = EffectiveDate::from_option(effective_from.cloned());
58 self.specs.remove(&key).is_some()
59 }
60
61 pub fn iter_specs(&self) -> impl Iterator<Item = &LemmaSpec> + '_ {
62 self.specs.values()
63 }
64
65 pub fn iter_with_ranges(
75 &self,
76 ) -> impl Iterator<Item = (&LemmaSpec, Option<DateTimeValue>, Option<DateTimeValue>)> + '_ {
77 self.iter_specs().map(move |spec| {
78 let (effective_from, effective_to) = self.effective_range(spec);
79 (spec, effective_from, effective_to)
80 })
81 }
82
83 #[must_use]
86 pub fn spec_at(&self, effective: &EffectiveDate) -> Option<&LemmaSpec> {
87 self.specs
88 .range(..=effective.clone())
89 .next_back()
90 .map(|(_, spec)| spec)
91 }
92
93 pub fn effective_range(
98 &self,
99 spec: &LemmaSpec,
100 ) -> (Option<DateTimeValue>, Option<DateTimeValue>) {
101 let from = spec.effective_from().cloned();
102 let key = spec.effective_from.clone();
103 let exact = self.specs.get_key_value(&key).unwrap_or_else(|| {
104 unreachable!(
105 "BUG: effective_range called with spec '{}' not in spec set",
106 spec.name
107 )
108 });
109 let to = self
110 .specs
111 .range((
112 std::ops::Bound::Excluded(exact.0),
113 std::ops::Bound::Unbounded,
114 ))
115 .next()
116 .and_then(|(_, next)| next.effective_from().cloned());
117 (from, to)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::parsing::ast::LemmaSpec;
125
126 fn main_repository() -> Arc<LemmaRepository> {
127 Arc::new(LemmaRepository::new(None))
128 }
129
130 use crate::literals::DateGranularity;
131
132 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
133 DateTimeValue {
134 year,
135 month,
136 day,
137 hour: 0,
138 minute: 0,
139 second: 0,
140 microsecond: 0,
141 timezone: None,
142 granularity: DateGranularity::Full,
143 }
144 }
145
146 fn make_spec(name: &str) -> LemmaSpec {
147 LemmaSpec::new(name.to_string())
148 }
149
150 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
151 let mut spec = LemmaSpec::new(name.to_string());
152 spec.effective_from = EffectiveDate::from_option(effective_from);
153 spec
154 }
155
156 #[test]
161 #[should_panic(expected = "BUG")]
162 fn insert_panics_on_spec_name_mismatch() {
163 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
164 ss.insert(make_spec("b"));
165 }
166
167 #[test]
168 fn effective_range_unbounded_single_spec() {
169 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
170 let spec = make_spec("a");
171 assert!(ss.insert(spec));
172 let stored = ss.get_exact(None).expect("inserted");
173
174 let (from, to) = ss.effective_range(stored);
175 assert_eq!(from, None);
176 assert_eq!(to, None);
177 }
178
179 #[test]
180 fn effective_range_soft_end_from_next_spec() {
181 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
182 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
183 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
184
185 let v1 = ss.get_exact(Some(&date(2025, 1, 1))).expect("v1");
186 let (from, to) = ss.effective_range(v1);
187 assert_eq!(from, Some(date(2025, 1, 1)));
188 assert_eq!(to, Some(date(2025, 6, 1)));
189
190 let v2 = ss.get_exact(Some(&date(2025, 6, 1))).expect("v2");
191 let (from, to) = ss.effective_range(v2);
192 assert_eq!(from, Some(date(2025, 6, 1)));
193 assert_eq!(to, None);
194 }
195
196 #[test]
200 fn iter_with_ranges_yields_specs_paired_with_half_open_range() {
201 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
202 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 1, 1)))));
203 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 6, 1)))));
204
205 let entries: Vec<_> = ss.iter_with_ranges().collect();
206 assert_eq!(entries.len(), 2);
207
208 let (spec_0, from_0, to_0) = &entries[0];
209 assert_eq!(spec_0.effective_from(), Some(&date(2025, 1, 1)));
210 assert_eq!(from_0, &Some(date(2025, 1, 1)));
211 assert_eq!(
212 to_0,
213 &Some(date(2025, 6, 1)),
214 "earlier row ends at the next row's effective_from"
215 );
216
217 let (spec_1, from_1, to_1) = &entries[1];
218 assert_eq!(spec_1.effective_from(), Some(&date(2025, 6, 1)));
219 assert_eq!(from_1, &Some(date(2025, 6, 1)));
220 assert_eq!(
221 to_1, &None,
222 "latest row has no successor; effective_to is None"
223 );
224 }
225
226 #[test]
227 fn effective_range_unbounded_start_with_successor() {
228 let mut ss = LemmaSpecSet::new(main_repository(), "a".to_string());
229 assert!(ss.insert(make_spec("a")));
230 assert!(ss.insert(make_spec_with_range("a", Some(date(2025, 3, 1)))));
231
232 let v1 = ss.get_exact(None).expect("v1");
233 let (from, to) = ss.effective_range(v1);
234 assert_eq!(from, None);
235 assert_eq!(to, Some(date(2025, 3, 1)));
236 }
237}