1use yo_common::{Code, Error, Result};
17
18#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum Num {
24 Int(i64),
26 Float(f64),
28}
29
30impl Num {
31 #[must_use]
33 pub const fn is_int(self) -> bool {
34 matches!(self, Num::Int(_))
35 }
36
37 #[must_use]
39 const fn zero_like(self) -> Num {
40 match self {
41 Num::Int(_) => Num::Int(0),
42 Num::Float(_) => Num::Float(0.0),
43 }
44 }
45
46 fn as_int(self, what: &str) -> Result<i64> {
47 match self {
48 Num::Int(n) => Ok(n),
49 Num::Float(_) => Err(Error::fmt(
50 Code::Invalid,
51 format_args!("{what} is not an integer or out of range"),
52 )),
53 }
54 }
55
56 fn as_float(self, what: &str) -> Result<f64> {
57 match self {
58 Num::Float(f) => Ok(f),
59 Num::Int(n) => {
60 let _ = what;
64 Ok(n as f64)
65 }
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum IncrExpire {
73 #[default]
75 Keep,
76 Persist,
78 At(u64),
80 AtIfNone(u64),
82}
83
84#[derive(Debug, Clone, Copy, PartialEq)]
86pub struct IncrEx {
87 pub by: Num,
89 pub saturate: bool,
91 pub lower: Option<Num>,
93 pub upper: Option<Num>,
95 pub expire: IncrExpire,
97}
98
99impl Default for IncrEx {
100 fn default() -> IncrEx {
101 IncrEx {
102 by: Num::Int(1),
103 saturate: false,
104 lower: None,
105 upper: None,
106 expire: IncrExpire::Keep,
107 }
108 }
109}
110
111impl IncrEx {
112 pub const PLAIN: IncrEx = IncrEx {
114 by: Num::Int(1),
115 saturate: false,
116 lower: None,
117 upper: None,
118 expire: IncrExpire::Keep,
119 };
120
121 #[must_use]
123 pub const fn by(mut self, by: Num) -> IncrEx {
124 self.by = by;
125 self
126 }
127
128 #[must_use]
130 pub const fn saturating(mut self) -> IncrEx {
131 self.saturate = true;
132 self
133 }
134
135 #[must_use]
137 pub const fn between(mut self, lower: Option<Num>, upper: Option<Num>) -> IncrEx {
138 self.lower = lower;
139 self.upper = upper;
140 self
141 }
142
143 #[must_use]
145 pub const fn expiring(mut self, expire: IncrExpire) -> IncrEx {
146 self.expire = expire;
147 self
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq)]
157pub struct Counted {
158 pub value: Num,
160 pub applied: Num,
162 pub stored: bool,
165}
166
167pub fn apply(current: Num, opts: &IncrEx) -> Result<Counted> {
174 match opts.by {
175 Num::Int(by) => {
176 let now = current.as_int("value")?;
177 let lo = opts.lower.map_or(Ok(i64::MIN), |b| b.as_int("LBOUND"))?;
178 let hi = opts.upper.map_or(Ok(i64::MAX), |b| b.as_int("UBOUND"))?;
179 if lo > hi {
180 return Err(bounds_crossed());
181 }
182 let want = now.checked_add(by);
183 let out = match want {
184 Some(v) if v >= lo && v <= hi => Some(v),
185 _ if !opts.saturate => None,
186 _ if by >= 0 => Some(hi),
190 _ => Some(lo),
191 };
192 Ok(match out {
193 Some(v) => Counted {
194 value: Num::Int(v),
195 applied: Num::Int(v.checked_sub(now).ok_or_else(applied_overflow)?),
202 stored: true,
203 },
204 None => Counted {
205 value: Num::Int(now),
206 applied: Num::Int(0),
207 stored: false,
208 },
209 })
210 }
211 Num::Float(by) => {
212 if by.is_nan() {
213 return Err(Error::new(Code::Invalid, "value is not a valid float"));
214 }
215 let now = match current {
216 Num::Float(f) => f,
217 Num::Int(n) => n as f64,
218 };
219 let lo = opts.lower.map_or(Ok(f64::MIN), |b| b.as_float("LBOUND"))?;
220 let hi = opts.upper.map_or(Ok(f64::MAX), |b| b.as_float("UBOUND"))?;
221 if lo > hi {
222 return Err(bounds_crossed());
223 }
224 let want = now + by;
225 let out = if want.is_finite() && want >= lo && want <= hi {
226 Some(want)
227 } else if !opts.saturate {
228 None
229 } else if by >= 0.0 {
230 Some(hi)
231 } else {
232 Some(lo)
233 };
234 Ok(match out {
235 Some(v) => Counted {
236 value: Num::Float(v),
237 applied: Num::Float(v - now),
238 stored: true,
239 },
240 None => Counted {
241 value: Num::Float(now),
242 applied: opts.by.zero_like(),
243 stored: false,
244 },
245 })
246 }
247 }
248}
249
250fn bounds_crossed() -> Error {
256 Error::new(Code::Invalid, "LBOUND can't be greater than UBOUND")
257}
258
259fn applied_overflow() -> Error {
267 Error::new(Code::Invalid, "applied increment would overflow")
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn int(n: i64) -> Num {
275 Num::Int(n)
276 }
277
278 #[test]
279 fn the_plain_form_adds_one() {
280 let c = apply(int(5), &IncrEx::PLAIN).unwrap();
281 assert_eq!(c.value, int(6));
282 assert_eq!(c.applied, int(1));
283 assert!(c.stored);
284 }
285
286 #[test]
287 fn a_result_past_a_bound_is_refused_and_nothing_is_written() {
288 let opts = IncrEx::PLAIN.by(int(10)).between(None, Some(int(5)));
292 let c = apply(int(0), &opts).unwrap();
293 assert_eq!(c.value, int(0));
294 assert_eq!(c.applied, int(0));
295 assert!(!c.stored);
296 }
297
298 #[test]
299 fn saturate_lands_on_the_bound_and_reports_what_it_managed() {
300 let opts = IncrEx::PLAIN
301 .by(int(10))
302 .between(None, Some(int(5)))
303 .saturating();
304 let c = apply(int(0), &opts).unwrap();
305 assert_eq!(c.value, int(5));
306 assert_eq!(c.applied, int(5));
307 assert!(c.stored);
308
309 let down = IncrEx::PLAIN
312 .by(int(-10))
313 .between(Some(int(0)), None)
314 .saturating();
315 let c = apply(int(5), &down).unwrap();
316 assert_eq!(c.value, int(0));
317 assert_eq!(c.applied, int(-5));
318 }
319
320 #[test]
321 fn overflow_is_a_bound_and_not_a_wrap() {
322 let c = apply(int(i64::MAX), &IncrEx::PLAIN).unwrap();
323 assert_eq!(c.value, int(i64::MAX));
324 assert_eq!(c.applied, int(0));
325 assert!(!c.stored);
326
327 let sat = apply(int(i64::MAX), &IncrEx::PLAIN.saturating()).unwrap();
328 assert_eq!(sat.value, int(i64::MAX));
329 assert_eq!(sat.applied, int(0));
330
331 let down = apply(int(i64::MIN), &IncrEx::PLAIN.by(int(-1)).saturating()).unwrap();
332 assert_eq!(down.value, int(i64::MIN));
333 assert_eq!(down.applied, int(0));
334 }
335
336 #[test]
337 fn an_amount_applied_that_does_not_fit_is_refused_rather_than_wrapped() {
338 let opts = IncrEx::PLAIN
343 .by(int(1))
344 .between(None, Some(int(i64::MIN)))
345 .saturating();
346 let e = apply(int(i64::MAX - 7), &opts).unwrap_err();
347 assert_eq!(e.message(), "applied increment would overflow");
348
349 let up = IncrEx::PLAIN
351 .by(int(-1))
352 .between(Some(int(i64::MAX)), None)
353 .saturating();
354 assert!(apply(int(i64::MIN + 7), &up).is_err());
355
356 let ok = IncrEx::PLAIN
359 .by(int(1))
360 .between(None, Some(int(i64::MIN + 8)))
361 .saturating();
362 let c = apply(int(-3), &ok).unwrap();
363 assert_eq!(c.value, int(i64::MIN + 8));
364 assert_eq!(c.applied, int(i64::MIN + 11));
365 assert!(c.stored);
366 }
367
368 #[test]
369 fn bounds_the_wrong_way_round_are_refused_rather_than_obeyed() {
370 let opts = IncrEx::PLAIN.between(Some(int(10)), Some(int(5)));
372 let e = apply(int(0), &opts).unwrap_err();
373 assert_eq!(e.message(), "LBOUND can't be greater than UBOUND");
374
375 let f = IncrEx::PLAIN
376 .by(Num::Float(1.0))
377 .between(Some(Num::Float(10.0)), Some(Num::Float(5.0)));
378 assert!(apply(Num::Float(0.0), &f).is_err());
379 }
380
381 #[test]
382 fn a_float_bound_on_an_integer_increment_is_an_error() {
383 let opts = IncrEx::PLAIN.between(None, Some(Num::Float(5.5)));
386 let e = apply(int(1), &opts).unwrap_err();
387 assert!(e.message().contains("UBOUND"), "{e}");
388 }
389
390 #[test]
391 fn a_float_increment_counts_in_floats() {
392 let c = apply(Num::Float(1.0), &IncrEx::PLAIN.by(Num::Float(0.5))).unwrap();
393 assert_eq!(c.value, Num::Float(1.5));
394 assert_eq!(c.applied, Num::Float(0.5));
395
396 let bounded = IncrEx::PLAIN
399 .by(Num::Float(10.0))
400 .between(None, Some(int(5)))
401 .saturating();
402 let c = apply(Num::Float(0.0), &bounded).unwrap();
403 assert_eq!(c.value, Num::Float(5.0));
404 }
405
406 #[test]
407 fn a_float_that_overflows_to_infinity_is_out_of_range() {
408 let opts = IncrEx::PLAIN.by(Num::Float(f64::MAX));
409 let c = apply(Num::Float(f64::MAX), &opts).unwrap();
410 assert!(!c.stored);
411 assert_eq!(c.value, Num::Float(f64::MAX));
412
413 let sat = apply(Num::Float(f64::MAX), &opts.saturating()).unwrap();
414 assert_eq!(sat.value, Num::Float(f64::MAX));
415 assert_eq!(sat.applied, Num::Float(0.0));
416 }
417}