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