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