1use chrono::{Datelike, Timelike};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::collections::BTreeMap;
8use std::fmt;
9
10use crate::computation::rational::{self, RationalInteger};
11
12pub type BaseMeasureVector = BTreeMap<String, i32>;
21
22pub fn rational_to_serialized_str(rational: &RationalInteger) -> Result<String, String> {
27 rational
28 .try_to_decimal_string()
29 .map_err(|failure| failure.to_string())
30}
31
32pub fn rational_from_parsed_decimal(decimal: Decimal) -> Result<RationalInteger, String> {
33 rational::decimal_to_rational(decimal).map_err(|failure| failure.to_string())
34}
35
36pub mod stored_rational_serde {
38 use super::{rational_from_parsed_decimal, rational_to_serialized_str, RationalInteger};
39 use rust_decimal::Decimal;
40 use serde::{Deserialize, Deserializer, Serializer};
41
42 pub fn serialize<S: Serializer>(
43 value: &RationalInteger,
44 serializer: S,
45 ) -> Result<S::Ok, S::Error> {
46 serializer.serialize_str(
47 &rational_to_serialized_str(value)
48 .expect("BUG: planned bound must serialize to decimal string"),
49 )
50 }
51
52 pub mod option {
53 use super::*;
54
55 pub fn serialize<S: Serializer>(
56 value: &Option<RationalInteger>,
57 serializer: S,
58 ) -> Result<S::Ok, S::Error> {
59 match value {
60 Some(rational) => super::serialize(rational, serializer),
61 None => serializer.serialize_none(),
62 }
63 }
64
65 pub fn deserialize<'de, D: Deserializer<'de>>(
66 deserializer: D,
67 ) -> Result<Option<RationalInteger>, D::Error> {
68 Option::<Decimal>::deserialize(deserializer)?
69 .map(rational_from_parsed_decimal)
70 .transpose()
71 .map_err(serde::de::Error::custom)
72 }
73 }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, Hash)]
84pub struct MeasureUnit {
85 pub name: String,
86 pub factor: RationalInteger,
88 pub derived_measure_factors: Vec<(String, i32)>,
89 pub decomposition: BaseMeasureVector,
90 pub minimum: Option<RationalInteger>,
92 pub maximum: Option<RationalInteger>,
94 pub suggestion_magnitude: Option<RationalInteger>,
96}
97
98impl Serialize for MeasureUnit {
99 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
100 use measure_unit_factor_serialization::FactorSerializer;
101 use serde::ser::SerializeStruct;
102 let mut state = serializer.serialize_struct("MeasureUnit", 7)?;
103 state.serialize_field("name", &self.name)?;
104 state.serialize_field("factor", &FactorSerializer::from_ratio(&self.factor))?;
105 state.serialize_field("derived_measure_factors", &self.derived_measure_factors)?;
106 state.serialize_field("decomposition", &self.decomposition)?;
107 if let Some(minimum) = &self.minimum {
108 state.serialize_field(
109 "minimum",
110 &rational_to_serialized_str(minimum)
111 .expect("BUG: planned measure unit minimum must serialize to decimal string"),
112 )?;
113 }
114 if let Some(maximum) = &self.maximum {
115 state.serialize_field(
116 "maximum",
117 &rational_to_serialized_str(maximum)
118 .expect("BUG: planned measure unit maximum must serialize to decimal string"),
119 )?;
120 }
121 if let Some(suggestion_magnitude) = &self.suggestion_magnitude {
122 state.serialize_field(
123 "suggestion",
124 &rational_to_serialized_str(suggestion_magnitude).expect(
125 "BUG: planned measure unit suggestion must serialize to decimal string",
126 ),
127 )?;
128 }
129 state.end()
130 }
131}
132
133impl<'de> Deserialize<'de> for MeasureUnit {
134 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
135 #[derive(Deserialize)]
136 struct MeasureUnitData {
137 name: String,
138 #[serde(with = "measure_unit_factor_serialization")]
139 factor: RationalInteger,
140 #[serde(default)]
141 derived_measure_factors: Vec<(String, i32)>,
142 #[serde(default)]
143 decomposition: BaseMeasureVector,
144 #[serde(default)]
145 minimum: Option<Decimal>,
146 #[serde(default)]
147 maximum: Option<Decimal>,
148 #[serde(default, rename = "suggestion")]
149 suggestion_magnitude: Option<Decimal>,
150 }
151 let data = MeasureUnitData::deserialize(deserializer)?;
152 Ok(Self {
153 name: data.name,
154 factor: data.factor,
155 derived_measure_factors: data.derived_measure_factors,
156 decomposition: data.decomposition,
157 minimum: data
158 .minimum
159 .map(rational_from_parsed_decimal)
160 .transpose()
161 .map_err(serde::de::Error::custom)?,
162 maximum: data
163 .maximum
164 .map(rational_from_parsed_decimal)
165 .transpose()
166 .map_err(serde::de::Error::custom)?,
167 suggestion_magnitude: data
168 .suggestion_magnitude
169 .map(rational_from_parsed_decimal)
170 .transpose()
171 .map_err(serde::de::Error::custom)?,
172 })
173 }
174}
175
176impl MeasureUnit {
177 pub fn from_decimal_factor(
178 name: String,
179 decimal_factor: Decimal,
180 derived_measure_factors: Vec<(String, i32)>,
181 ) -> Result<Self, String> {
182 let factor =
183 rational::decimal_to_rational(decimal_factor).map_err(|failure| failure.to_string())?;
184 Ok(MeasureUnit {
185 name,
186 factor,
187 derived_measure_factors,
188 decomposition: BaseMeasureVector::new(),
189 minimum: None,
190 maximum: None,
191 suggestion_magnitude: None,
192 })
193 }
194
195 pub fn clear_constraint_magnitudes(&mut self) {
196 self.minimum = None;
197 self.maximum = None;
198 self.suggestion_magnitude = None;
199 }
200
201 pub fn is_canonical_factor(&self) -> bool {
202 self.factor == rational::rational_one()
203 }
204
205 pub fn is_positive_factor(&self) -> bool {
206 let numerator = self.factor.numer();
207 let denominator = self.factor.denom();
208 !numerator.is_zero() && numerator.is_positive() == denominator.is_positive()
209 }
210
211 pub fn factor_decimal(&self) -> Decimal {
213 rational::RationalInteger::try_to_decimal(&self.factor)
214 .expect("BUG: measure unit factor must materialize to decimal")
215 }
216
217 #[must_use]
218 pub fn minimum_decimal(&self) -> Option<Decimal> {
219 self.minimum.as_ref().map(|bound| {
220 bound
221 .try_to_decimal()
222 .expect("BUG: planned measure unit minimum must materialize to decimal")
223 })
224 }
225
226 #[must_use]
227 pub fn maximum_decimal(&self) -> Option<Decimal> {
228 self.maximum.as_ref().map(|bound| {
229 bound
230 .try_to_decimal()
231 .expect("BUG: planned measure unit maximum must materialize to decimal")
232 })
233 }
234
235 #[must_use]
236 pub fn suggestion_magnitude_decimal(&self) -> Option<Decimal> {
237 self.suggestion_magnitude.as_ref().map(|bound| {
238 bound
239 .try_to_decimal()
240 .expect("BUG: planned measure unit default must materialize to decimal")
241 })
242 }
243
244 #[must_use]
246 pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
247 self.maximum.as_ref().map(|maximum| {
248 let canonical = rational::checked_mul(maximum, &self.factor)
249 .expect("BUG: planned measure unit maximum canonical multiply must succeed");
250 canonical
251 .try_to_decimal()
252 .expect("BUG: planned measure unit maximum canonical must materialize to decimal")
253 })
254 }
255}
256
257mod measure_unit_factor_serialization {
258 use super::RationalInteger;
259 use crate::computation::bigint::BigInt;
260 use crate::computation::rational::try_rational_new;
261 use serde::{Deserialize, Serialize};
262
263 #[derive(Serialize, Deserialize)]
264 pub struct FactorSerializer {
265 numer: String,
266 denom: String,
267 }
268
269 impl FactorSerializer {
270 pub fn from_ratio(value: &RationalInteger) -> Self {
271 let reduced = value
272 .clone()
273 .try_reduce()
274 .expect("BUG: stored measure unit factor must reduce");
275 FactorSerializer {
276 numer: reduced.numer().to_string(),
277 denom: reduced.denom().to_string(),
278 }
279 }
280
281 pub fn into_ratio(self) -> Result<RationalInteger, String> {
282 let numer = BigInt::try_from_str_radix(&self.numer, 10)
283 .map_err(|_| format!("invalid numerator: {}", self.numer))?;
284 let denom = BigInt::try_from_str_radix(&self.denom, 10)
285 .map_err(|_| format!("invalid denominator: {}", self.denom))?;
286 if denom.is_zero() {
287 return Err("MeasureUnit conversion factor denominator cannot be zero".to_string());
288 }
289 try_rational_new(numer, denom).map_err(|e| e.to_string())
290 }
291 }
292
293 pub fn deserialize<'de, D: serde::Deserializer<'de>>(
294 deserializer: D,
295 ) -> Result<RationalInteger, D::Error> {
296 FactorSerializer::deserialize(deserializer)?
297 .into_ratio()
298 .map_err(serde::de::Error::custom)
299 }
300}
301
302#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
303#[serde(transparent)]
304pub struct MeasureUnits(pub Vec<MeasureUnit>);
305
306impl MeasureUnits {
307 pub fn new() -> Self {
308 MeasureUnits(Vec::new())
309 }
310 pub fn get(&self, name: &str) -> Result<&MeasureUnit, String> {
311 self.0.iter().find(|u| u.name == name).ok_or_else(|| {
312 let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
313 format!(
314 "Unknown unit '{}' for this measure type. Valid units: {}",
315 name,
316 valid.join(", ")
317 )
318 })
319 }
320
321 pub fn iter(&self) -> std::slice::Iter<'_, MeasureUnit> {
322 self.0.iter()
323 }
324 pub fn push(&mut self, u: MeasureUnit) {
325 self.0.push(u);
326 }
327 pub fn is_empty(&self) -> bool {
328 self.0.is_empty()
329 }
330 pub fn len(&self) -> usize {
331 self.0.len()
332 }
333 pub fn map<F: FnMut(MeasureUnit) -> MeasureUnit>(self, f: F) -> Self {
334 MeasureUnits(self.0.into_iter().map(f).collect())
335 }
336}
337
338impl MeasureUnit {
339 pub fn with_decomposition(self, decomposition: BaseMeasureVector) -> Self {
340 Self {
341 decomposition,
342 ..self
343 }
344 }
345 pub fn with_factor(self, factor: RationalInteger) -> Self {
346 Self { factor, ..self }
347 }
348 pub fn with_derived_measure_factors(self, derived_measure_factors: Vec<(String, i32)>) -> Self {
349 Self {
350 derived_measure_factors,
351 ..self
352 }
353 }
354}
355
356impl Default for MeasureUnits {
357 fn default() -> Self {
358 MeasureUnits::new()
359 }
360}
361
362impl From<Vec<MeasureUnit>> for MeasureUnits {
363 fn from(v: Vec<MeasureUnit>) -> Self {
364 MeasureUnits(v)
365 }
366}
367
368impl<'a> IntoIterator for &'a MeasureUnits {
369 type Item = &'a MeasureUnit;
370 type IntoIter = std::slice::Iter<'a, MeasureUnit>;
371 fn into_iter(self) -> Self::IntoIter {
372 self.0.iter()
373 }
374}
375
376#[derive(Clone, Debug, PartialEq, Eq, Hash)]
377pub struct RatioUnit {
378 pub name: String,
379 pub value: RationalInteger,
380 pub minimum: Option<RationalInteger>,
381 pub maximum: Option<RationalInteger>,
382 pub suggestion_magnitude: Option<RationalInteger>,
383}
384
385impl Serialize for RatioUnit {
386 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
387 use measure_unit_factor_serialization::FactorSerializer;
388 use serde::ser::SerializeStruct;
389 let mut state = serializer.serialize_struct("RatioUnit", 5)?;
390 state.serialize_field("name", &self.name)?;
391 state.serialize_field("value", &FactorSerializer::from_ratio(&self.value))?;
392 if let Some(minimum) = &self.minimum {
393 state.serialize_field(
394 "minimum",
395 &rational_to_serialized_str(minimum)
396 .expect("BUG: planned ratio unit minimum must serialize to decimal string"),
397 )?;
398 }
399 if let Some(maximum) = &self.maximum {
400 state.serialize_field(
401 "maximum",
402 &rational_to_serialized_str(maximum)
403 .expect("BUG: planned ratio unit maximum must serialize to decimal string"),
404 )?;
405 }
406 if let Some(suggestion_magnitude) = &self.suggestion_magnitude {
407 state.serialize_field(
408 "suggestion",
409 &rational_to_serialized_str(suggestion_magnitude)
410 .expect("BUG: planned ratio unit suggestion must serialize to decimal string"),
411 )?;
412 }
413 state.end()
414 }
415}
416
417impl<'de> Deserialize<'de> for RatioUnit {
418 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
419 #[derive(Deserialize)]
420 struct RatioUnitData {
421 name: String,
422 #[serde(with = "measure_unit_factor_serialization")]
423 value: RationalInteger,
424 #[serde(default)]
425 minimum: Option<Decimal>,
426 #[serde(default)]
427 maximum: Option<Decimal>,
428 #[serde(default, rename = "suggestion")]
429 suggestion_magnitude: Option<Decimal>,
430 }
431 let data = RatioUnitData::deserialize(deserializer)?;
432 Ok(Self {
433 name: data.name,
434 value: data.value,
435 minimum: data
436 .minimum
437 .map(rational_from_parsed_decimal)
438 .transpose()
439 .map_err(serde::de::Error::custom)?,
440 maximum: data
441 .maximum
442 .map(rational_from_parsed_decimal)
443 .transpose()
444 .map_err(serde::de::Error::custom)?,
445 suggestion_magnitude: data
446 .suggestion_magnitude
447 .map(rational_from_parsed_decimal)
448 .transpose()
449 .map_err(serde::de::Error::custom)?,
450 })
451 }
452}
453
454impl RatioUnit {
455 pub fn clear_constraint_magnitudes(&mut self) {
456 self.minimum = None;
457 self.maximum = None;
458 self.suggestion_magnitude = None;
459 }
460
461 pub fn value_decimal(&self) -> Decimal {
463 self.value
464 .try_to_decimal()
465 .expect("BUG: ratio unit value must materialize to decimal")
466 }
467
468 #[must_use]
469 pub fn minimum_decimal(&self) -> Option<Decimal> {
470 self.minimum.as_ref().map(|bound| {
471 bound
472 .try_to_decimal()
473 .expect("BUG: planned ratio unit minimum must materialize to decimal")
474 })
475 }
476
477 #[must_use]
478 pub fn maximum_decimal(&self) -> Option<Decimal> {
479 self.maximum.as_ref().map(|bound| {
480 bound
481 .try_to_decimal()
482 .expect("BUG: planned ratio unit maximum must materialize to decimal")
483 })
484 }
485
486 #[must_use]
487 pub fn suggestion_magnitude_decimal(&self) -> Option<Decimal> {
488 self.suggestion_magnitude.as_ref().map(|bound| {
489 bound
490 .try_to_decimal()
491 .expect("BUG: planned ratio unit default must materialize to decimal")
492 })
493 }
494
495 #[must_use]
497 pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
498 self.maximum.as_ref().map(|maximum| {
499 let canonical = rational::checked_mul(maximum, &self.value)
500 .expect("BUG: planned ratio unit maximum canonical multiply must succeed");
501 canonical
502 .try_to_decimal()
503 .expect("BUG: planned ratio unit maximum canonical must materialize to decimal")
504 })
505 }
506}
507
508#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
509#[serde(transparent)]
510pub struct RatioUnits(pub Vec<RatioUnit>);
511
512impl RatioUnits {
513 pub fn new() -> Self {
514 RatioUnits(Vec::new())
515 }
516 pub fn get(&self, name: &str) -> Result<&RatioUnit, String> {
517 self.0.iter().find(|u| u.name == name).ok_or_else(|| {
518 let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
519 format!(
520 "Unknown unit '{}' for this ratio type. Valid units: {}",
521 name,
522 valid.join(", ")
523 )
524 })
525 }
526
527 pub fn iter(&self) -> std::slice::Iter<'_, RatioUnit> {
528 self.0.iter()
529 }
530 pub fn push(&mut self, u: RatioUnit) {
531 self.0.push(u);
532 }
533 pub fn is_empty(&self) -> bool {
534 self.0.is_empty()
535 }
536 pub fn len(&self) -> usize {
537 self.0.len()
538 }
539}
540
541impl Default for RatioUnits {
542 fn default() -> Self {
543 RatioUnits::new()
544 }
545}
546
547impl From<Vec<RatioUnit>> for RatioUnits {
548 fn from(v: Vec<RatioUnit>) -> Self {
549 RatioUnits(v)
550 }
551}
552
553impl<'a> IntoIterator for &'a RatioUnits {
554 type Item = &'a RatioUnit;
555 type IntoIter = std::slice::Iter<'a, RatioUnit>;
556 fn into_iter(self) -> Self::IntoIter {
557 self.0.iter()
558 }
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
566#[serde(rename_all = "lowercase")]
567pub enum BooleanValue {
568 True,
569 False,
570 Yes,
571 No,
572}
573
574impl From<BooleanValue> for bool {
575 fn from(value: BooleanValue) -> bool {
576 matches!(value, BooleanValue::True | BooleanValue::Yes)
577 }
578}
579
580impl From<&BooleanValue> for bool {
581 fn from(value: &BooleanValue) -> bool {
582 (*value).into() }
584}
585
586impl From<bool> for BooleanValue {
587 fn from(value: bool) -> BooleanValue {
588 if value {
589 BooleanValue::True
590 } else {
591 BooleanValue::False
592 }
593 }
594}
595
596impl std::ops::Not for BooleanValue {
597 type Output = BooleanValue;
598
599 fn not(self) -> Self::Output {
600 if self.into() {
601 BooleanValue::False
602 } else {
603 BooleanValue::True
604 }
605 }
606}
607
608impl std::ops::Not for &BooleanValue {
609 type Output = BooleanValue;
610
611 fn not(self) -> Self::Output {
612 if (*self).into() {
613 BooleanValue::False
614 } else {
615 BooleanValue::True
616 }
617 }
618}
619
620impl std::str::FromStr for BooleanValue {
621 type Err = String;
622
623 fn from_str(s: &str) -> Result<Self, Self::Err> {
624 match s.trim().to_lowercase().as_str() {
625 "true" => Ok(BooleanValue::True),
626 "false" => Ok(BooleanValue::False),
627 "yes" => Ok(BooleanValue::Yes),
628 "no" => Ok(BooleanValue::No),
629 _ => Err(format!("Invalid boolean: '{}'", s)),
630 }
631 }
632}
633
634impl BooleanValue {
635 #[must_use]
636 pub fn as_str(&self) -> &'static str {
637 match self {
638 BooleanValue::True => "true",
639 BooleanValue::False => "false",
640 BooleanValue::Yes => "yes",
641 BooleanValue::No => "no",
642 }
643 }
644}
645
646impl fmt::Display for BooleanValue {
647 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648 write!(f, "{}", self.as_str())
649 }
650}
651
652#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
653pub struct TimezoneValue {
654 pub offset_hours: i8,
655 pub offset_minutes: u8,
656}
657
658impl fmt::Display for TimezoneValue {
659 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
660 if self.offset_hours == 0 && self.offset_minutes == 0 {
661 write!(f, "Z")
662 } else {
663 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
664 let hour = self.offset_hours.abs();
665 write!(f, "{}{:02}:{:02}", sign, hour, self.offset_minutes)
666 }
667 }
668}
669
670#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
671pub struct TimeValue {
672 pub hour: u8,
673 pub minute: u8,
674 pub second: u8,
675 #[serde(default)]
676 pub microsecond: u32,
677 pub timezone: Option<TimezoneValue>,
678}
679
680impl fmt::Display for TimeValue {
681 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
682 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
683 if self.microsecond != 0 {
684 write!(f, ".{:06}", self.microsecond)?;
685 }
686 if let Some(timezone) = &self.timezone {
687 write!(f, "{}", timezone)?;
688 }
689 Ok(())
690 }
691}
692
693#[derive(
694 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
695)]
696#[serde(rename_all = "snake_case")]
697pub enum DateGranularity {
698 Year,
699 YearMonth,
700 IsoWeek {
704 iso_year: i32,
705 week: u32,
706 },
707 #[default]
708 Full,
709 DateTime,
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct DateTimeValue {
714 pub year: i32,
715 pub month: u32,
716 pub day: u32,
717 pub hour: u32,
718 pub minute: u32,
719 pub second: u32,
720 #[serde(default)]
721 pub microsecond: u32,
722 pub timezone: Option<TimezoneValue>,
723 #[serde(default)]
724 pub granularity: DateGranularity,
725}
726
727impl PartialEq for DateTimeValue {
728 fn eq(&self, other: &Self) -> bool {
729 self.year == other.year
730 && self.month == other.month
731 && self.day == other.day
732 && self.hour == other.hour
733 && self.minute == other.minute
734 && self.second == other.second
735 && self.microsecond == other.microsecond
736 && self.timezone == other.timezone
737 }
738}
739
740impl Eq for DateTimeValue {}
741
742impl PartialOrd for DateTimeValue {
743 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
744 Some(self.cmp(other))
745 }
746}
747
748impl Ord for DateTimeValue {
749 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
750 self.year
751 .cmp(&other.year)
752 .then_with(|| self.month.cmp(&other.month))
753 .then_with(|| self.day.cmp(&other.day))
754 .then_with(|| self.hour.cmp(&other.hour))
755 .then_with(|| self.minute.cmp(&other.minute))
756 .then_with(|| self.second.cmp(&other.second))
757 .then_with(|| self.microsecond.cmp(&other.microsecond))
758 .then_with(|| self.timezone.cmp(&other.timezone))
759 }
760}
761
762impl std::hash::Hash for DateTimeValue {
763 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
764 self.year.hash(state);
765 self.month.hash(state);
766 self.day.hash(state);
767 self.hour.hash(state);
768 self.minute.hash(state);
769 self.second.hash(state);
770 self.microsecond.hash(state);
771 self.timezone.hash(state);
772 }
773}
774
775impl DateTimeValue {
776 pub fn now() -> Self {
777 let now = chrono::Local::now();
778 let offset_secs = now.offset().local_minus_utc();
779 Self {
780 year: now.year(),
781 month: now.month(),
782 day: now.day(),
783 hour: now.time().hour(),
784 minute: now.time().minute(),
785 second: now.time().second(),
786 microsecond: now.time().nanosecond() / 1000 % 1_000_000,
787 timezone: Some(TimezoneValue {
788 offset_hours: (offset_secs / 3600) as i8,
789 offset_minutes: ((offset_secs.abs() % 3600) / 60) as u8,
790 }),
791 granularity: DateGranularity::DateTime,
792 }
793 }
794
795 fn parse_iso_week(s: &str) -> Option<Self> {
796 let parts: Vec<&str> = s.split("-W").collect();
797 if parts.len() != 2 {
798 return None;
799 }
800 let iso_year: i32 = parts[0].parse().ok()?;
801 let week: u32 = parts[1].parse().ok()?;
802 if week == 0 || week > 53 {
803 return None;
804 }
805 let date = chrono::NaiveDate::from_isoywd_opt(iso_year, week, chrono::Weekday::Mon)?;
806 Some(Self {
807 year: date.year(),
808 month: date.month(),
809 day: date.day(),
810 hour: 0,
811 minute: 0,
812 second: 0,
813 microsecond: 0,
814 timezone: None,
815 granularity: DateGranularity::IsoWeek { iso_year, week },
816 })
817 }
818}
819
820impl fmt::Display for DateTimeValue {
821 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
822 match self.granularity {
823 DateGranularity::Year => write!(f, "{:04}", self.year),
824 DateGranularity::YearMonth => write!(f, "{:04}-{:02}", self.year, self.month),
825 DateGranularity::IsoWeek { iso_year, week } => {
826 write!(f, "{:04}-W{:02}", iso_year, week)
827 }
828 DateGranularity::Full => {
829 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
830 }
831 DateGranularity::DateTime => {
832 write!(
833 f,
834 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
835 self.year, self.month, self.day, self.hour, self.minute, self.second
836 )?;
837 if self.microsecond != 0 {
838 write!(f, ".{:06}", self.microsecond)?;
839 }
840 if let Some(tz) = &self.timezone {
841 write!(f, "{}", tz)?;
842 }
843 Ok(())
844 }
845 }
846 }
847}
848
849#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
854#[serde(rename_all = "snake_case")]
855pub enum Value {
856 Number(Decimal),
857 NumberWithUnit(Decimal, String),
858 Text(String),
859 Date(DateTimeValue),
860 Time(TimeValue),
861 Boolean(BooleanValue),
862 Range(Box<Value>, Box<Value>),
863}
864
865impl fmt::Display for Value {
866 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
867 match self {
868 Value::Number(n) => write!(f, "{}", n),
869 Value::Text(s) => write!(f, "{}", s),
870 Value::Date(dt) => write!(f, "{}", dt),
871 Value::Boolean(b) => write!(f, "{}", b),
872 Value::Time(time) => write!(f, "{}", time),
873 Value::NumberWithUnit(n, u) => match u.as_str() {
874 "percent" => {
875 let norm = n.normalize();
876 let s = if norm.fract().is_zero() {
877 norm.trunc().to_string()
878 } else {
879 norm.to_string()
880 };
881 write!(f, "{}%", s)
882 }
883 "permille" => {
884 let norm = n.normalize();
885 let s = if norm.fract().is_zero() {
886 norm.trunc().to_string()
887 } else {
888 norm.to_string()
889 };
890 write!(f, "{}%%", s)
891 }
892 unit => {
893 let norm = n.normalize();
894 let s = if norm.fract().is_zero() {
895 norm.trunc().to_string()
896 } else {
897 norm.to_string()
898 };
899 write!(f, "{} {}", s, unit)
900 }
901 },
902 Value::Range(left, right) => write!(f, "{}...{}", left, right),
903 }
904 }
905}
906
907impl std::str::FromStr for DateTimeValue {
912 type Err = String;
913
914 fn from_str(s: &str) -> Result<Self, Self::Err> {
915 if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
916 let offset = dt.offset().local_minus_utc();
917 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
918 return Ok(DateTimeValue {
919 year: dt.year(),
920 month: dt.month(),
921 day: dt.day(),
922 hour: dt.hour(),
923 minute: dt.minute(),
924 second: dt.second(),
925 microsecond,
926 timezone: Some(TimezoneValue {
927 offset_hours: (offset / 3600) as i8,
928 offset_minutes: ((offset.abs() % 3600) / 60) as u8,
929 }),
930 granularity: DateGranularity::DateTime,
931 });
932 }
933 if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
934 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
935 return Ok(DateTimeValue {
936 year: dt.year(),
937 month: dt.month(),
938 day: dt.day(),
939 hour: dt.hour(),
940 minute: dt.minute(),
941 second: dt.second(),
942 microsecond,
943 timezone: None,
944 granularity: DateGranularity::DateTime,
945 });
946 }
947 if let Ok(d) = s.parse::<chrono::NaiveDate>() {
948 return Ok(DateTimeValue {
949 year: d.year(),
950 month: d.month(),
951 day: d.day(),
952 hour: 0,
953 minute: 0,
954 second: 0,
955 microsecond: 0,
956 timezone: None,
957 granularity: DateGranularity::Full,
958 });
959 }
960 if let Some(week_val) = Self::parse_iso_week(s) {
961 return Ok(week_val);
962 }
963 if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
964 return Ok(Self {
965 year: ym.year(),
966 month: ym.month(),
967 day: 1,
968 hour: 0,
969 minute: 0,
970 second: 0,
971 microsecond: 0,
972 timezone: None,
973 granularity: DateGranularity::YearMonth,
974 });
975 }
976 if let Ok(year) = s.parse::<i32>() {
977 if (1..=9999).contains(&year) {
978 return Ok(Self {
979 year,
980 month: 1,
981 day: 1,
982 hour: 0,
983 minute: 0,
984 second: 0,
985 microsecond: 0,
986 timezone: None,
987 granularity: DateGranularity::Year,
988 });
989 }
990 }
991 Err(format!("Invalid date format: '{}'", s))
992 }
993}
994
995impl std::str::FromStr for TimeValue {
996 type Err = String;
997
998 fn from_str(s: &str) -> Result<Self, Self::Err> {
999 let trimmed = s.trim();
1000
1001 let (time_text, timezone) = if trimmed.ends_with('Z') || trimmed.ends_with('z') {
1002 (
1003 &trimmed[..trimmed.len() - 1],
1004 Some(TimezoneValue {
1005 offset_hours: 0,
1006 offset_minutes: 0,
1007 }),
1008 )
1009 } else if trimmed.len() > 1 {
1010 if let Some(sign_index) = trimmed[1..].rfind(['+', '-']).map(|index| index + 1) {
1011 let timezone_text = &trimmed[sign_index..];
1012 if timezone_text.len() == 6
1013 && (timezone_text.starts_with('+') || timezone_text.starts_with('-'))
1014 && timezone_text.as_bytes()[3] == b':'
1015 {
1016 let offset_hours: i8 = timezone_text[1..3]
1017 .parse()
1018 .map_err(|_| format!("Invalid time format: '{}'", s))?;
1019 let offset_minutes: u8 = timezone_text[4..6]
1020 .parse()
1021 .map_err(|_| format!("Invalid time format: '{}'", s))?;
1022 let signed_hours = if timezone_text.starts_with('-') {
1023 -offset_hours
1024 } else {
1025 offset_hours
1026 };
1027 (
1028 &trimmed[..sign_index],
1029 Some(TimezoneValue {
1030 offset_hours: signed_hours,
1031 offset_minutes,
1032 }),
1033 )
1034 } else {
1035 (trimmed, None)
1036 }
1037 } else {
1038 (trimmed, None)
1039 }
1040 } else {
1041 (trimmed, None)
1042 };
1043
1044 if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S%.f") {
1045 return Ok(TimeValue {
1046 hour: t.hour() as u8,
1047 minute: t.minute() as u8,
1048 second: t.second() as u8,
1049 microsecond: t.nanosecond() / 1000 % 1_000_000,
1050 timezone,
1051 });
1052 }
1053 if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S") {
1054 return Ok(TimeValue {
1055 hour: t.hour() as u8,
1056 minute: t.minute() as u8,
1057 second: t.second() as u8,
1058 microsecond: 0,
1059 timezone,
1060 });
1061 }
1062 if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M") {
1063 return Ok(TimeValue {
1064 hour: t.hour() as u8,
1065 minute: t.minute() as u8,
1066 second: 0,
1067 microsecond: 0,
1068 timezone,
1069 });
1070 }
1071 Err(format!("Invalid time format: '{}'", s))
1072 }
1073}
1074
1075pub(crate) struct NumberLiteral(pub Decimal);
1081
1082impl std::str::FromStr for NumberLiteral {
1083 type Err = String;
1084
1085 fn from_str(s: &str) -> Result<Self, Self::Err> {
1086 let clean = s.trim().replace(['_', ','], "");
1087 Decimal::from_str(&clean)
1088 .map_err(|_| format!("Invalid number: '{}'", s))
1089 .map(NumberLiteral)
1090 }
1091}
1092
1093pub(crate) struct TextLiteral(pub String);
1095
1096impl std::str::FromStr for TextLiteral {
1097 type Err = String;
1098
1099 fn from_str(s: &str) -> Result<Self, Self::Err> {
1100 if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
1101 return Err(format!(
1102 "Text value exceeds maximum length (max {} characters)",
1103 crate::limits::MAX_TEXT_VALUE_LENGTH
1104 ));
1105 }
1106 Ok(TextLiteral(s.to_string()))
1107 }
1108}
1109
1110pub(crate) struct NumberWithUnit(pub Decimal, pub String);
1112
1113impl std::str::FromStr for NumberWithUnit {
1114 type Err = String;
1115
1116 fn from_str(s: &str) -> Result<Self, Self::Err> {
1117 let trimmed = s.trim();
1118 if trimmed.is_empty() {
1119 return Err(
1120 "Measure value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
1121 .to_string(),
1122 );
1123 }
1124
1125 let mut parts = trimmed.split_whitespace();
1126 let number_part = parts
1127 .next()
1128 .expect("split_whitespace yields >=1 token after non-empty guard");
1129 let unit_part = parts.next().ok_or_else(|| {
1130 format!(
1131 "Measure value must include a unit (e.g. '{} eur').",
1132 number_part
1133 )
1134 })?;
1135 if parts.next().is_some() {
1136 return Err(format!(
1137 "Invalid measure value: '{}'. Expected exactly '<number> <unit>', got extra tokens.",
1138 s
1139 ));
1140 }
1141 let n = number_part
1142 .parse::<NumberLiteral>()
1143 .map_err(|_| format!("Invalid measure: '{}'", s))?
1144 .0;
1145 Ok(NumberWithUnit(n, unit_part.to_string()))
1146 }
1147}
1148
1149#[derive(Debug, Clone, PartialEq, Eq)]
1174pub(crate) enum RatioLiteral {
1175 Bare(Decimal),
1176 Percent(Decimal),
1177 Permille(Decimal),
1178 Named { value: Decimal, unit: String },
1179}
1180
1181impl std::str::FromStr for RatioLiteral {
1182 type Err = String;
1183
1184 fn from_str(s: &str) -> Result<Self, Self::Err> {
1185 let trimmed = s.trim();
1186 if trimmed.is_empty() {
1187 return Err(
1188 "Ratio value cannot be empty. Use a number, optionally followed by '%', '%%', or a unit name (e.g. '0.5', '50%', '25%%', '50 percent')."
1189 .to_string(),
1190 );
1191 }
1192
1193 let mut parts = trimmed.split_whitespace();
1194 let first = parts
1195 .next()
1196 .expect("split_whitespace yields >=1 token after non-empty guard");
1197 let second = parts.next();
1198 if parts.next().is_some() {
1199 return Err(format!(
1200 "Invalid ratio value: '{}'. Expected '<number>', '<number>%', '<number>%%', or '<number> <unit>'.",
1201 s
1202 ));
1203 }
1204
1205 match second {
1206 None => {
1208 if let Some(rest) = first.strip_suffix("%%") {
1209 if rest.is_empty() {
1210 return Err(format!(
1211 "Invalid ratio value: '{}'. '%%' must follow a number (e.g. '25%%').",
1212 s
1213 ));
1214 }
1215 let n = rest
1216 .parse::<NumberLiteral>()
1217 .map_err(|_| {
1218 format!(
1219 "Invalid ratio value: '{}'. '{}' is not a valid number before '%%'.",
1220 s, rest
1221 )
1222 })?
1223 .0;
1224 return Ok(RatioLiteral::Permille(n));
1225 }
1226 if let Some(rest) = first.strip_suffix('%') {
1227 if rest.is_empty() {
1228 return Err(format!(
1229 "Invalid ratio value: '{}'. '%' must follow a number (e.g. '50%').",
1230 s
1231 ));
1232 }
1233 let n = rest
1234 .parse::<NumberLiteral>()
1235 .map_err(|_| {
1236 format!(
1237 "Invalid ratio value: '{}'. '{}' is not a valid number before '%'.",
1238 s, rest
1239 )
1240 })?
1241 .0;
1242 return Ok(RatioLiteral::Percent(n));
1243 }
1244 let n = first.parse::<NumberLiteral>().map_err(|_| {
1245 format!(
1246 "Invalid ratio value: '{}'. Must be a number, '<n>%', '<n>%%', '<n> percent', '<n> permille', or '<n> <unit>'.",
1247 s
1248 )
1249 })?.0;
1250 Ok(RatioLiteral::Bare(n))
1251 }
1252 Some(unit) => {
1254 if unit == "%" || unit == "%%" {
1255 return Err(format!(
1256 "Invalid ratio value: '{}'. '{}' must be glued to the number (e.g. '{}{}'), not separated by whitespace.",
1257 s, unit, first, unit
1258 ));
1259 }
1260 let n = first
1261 .parse::<NumberLiteral>()
1262 .map_err(|_| {
1263 format!(
1264 "Invalid ratio value: '{}'. '{}' is not a valid number.",
1265 s, first
1266 )
1267 })?
1268 .0;
1269 Ok(RatioLiteral::Named {
1270 value: n,
1271 unit: unit.to_string(),
1272 })
1273 }
1274 }
1275 }
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280 use super::BooleanValue;
1281 use std::str::FromStr;
1282
1283 #[test]
1284 fn boolean_value_rejects_accept_and_reject_strings() {
1285 for invalid in ["accept", "reject"] {
1286 assert!(
1287 BooleanValue::from_str(invalid).is_err(),
1288 "'{invalid}' must not parse as boolean"
1289 );
1290 }
1291 }
1292}