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