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 BaseQuantityVector = 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 QuantityUnit {
83 pub name: String,
84 pub factor: RationalInteger,
86 pub derived_quantity_factors: Vec<(String, i32)>,
87 pub decomposition: BaseQuantityVector,
88 pub minimum: Option<RationalInteger>,
90 pub maximum: Option<RationalInteger>,
92 pub default_magnitude: Option<RationalInteger>,
94}
95
96impl Serialize for QuantityUnit {
97 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
98 use quantity_unit_factor_serialization::FactorSerializer;
99 use serde::ser::SerializeStruct;
100 let mut state = serializer.serialize_struct("QuantityUnit", 7)?;
101 state.serialize_field("name", &self.name)?;
102 state.serialize_field("factor", &FactorSerializer::from_ratio(&self.factor))?;
103 state.serialize_field("derived_quantity_factors", &self.derived_quantity_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 quantity 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 quantity 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 quantity unit default must serialize to decimal string"),
124 )?;
125 }
126 state.end()
127 }
128}
129
130impl<'de> Deserialize<'de> for QuantityUnit {
131 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
132 #[derive(Deserialize)]
133 struct QuantityUnitData {
134 name: String,
135 #[serde(with = "quantity_unit_factor_serialization")]
136 factor: RationalInteger,
137 #[serde(default)]
138 derived_quantity_factors: Vec<(String, i32)>,
139 #[serde(default)]
140 decomposition: BaseQuantityVector,
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 = QuantityUnitData::deserialize(deserializer)?;
149 Ok(Self {
150 name: data.name,
151 factor: data.factor,
152 derived_quantity_factors: data.derived_quantity_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 QuantityUnit {
174 pub fn from_decimal_factor(
175 name: String,
176 decimal_factor: Decimal,
177 derived_quantity_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(QuantityUnit {
182 name,
183 factor,
184 derived_quantity_factors,
185 decomposition: BaseQuantityVector::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: quantity 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 quantity 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 quantity 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 quantity 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 quantity unit maximum canonical multiply must succeed");
244 rational::commit_rational_to_decimal(&canonical)
245 .expect("BUG: planned quantity unit maximum canonical must commit to decimal")
246 })
247 }
248}
249
250mod quantity_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 quantity 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("QuantityUnit 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 QuantityUnits(pub Vec<QuantityUnit>);
298
299impl QuantityUnits {
300 pub fn new() -> Self {
301 QuantityUnits(Vec::new())
302 }
303 pub fn get(&self, name: &str) -> Result<&QuantityUnit, 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 quantity type. Valid units: {}",
308 name,
309 valid.join(", ")
310 )
311 })
312 }
313
314 pub fn iter(&self) -> std::slice::Iter<'_, QuantityUnit> {
315 self.0.iter()
316 }
317 pub fn push(&mut self, u: QuantityUnit) {
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(QuantityUnit) -> QuantityUnit>(self, f: F) -> Self {
327 QuantityUnits(self.0.into_iter().map(f).collect())
328 }
329}
330
331impl QuantityUnit {
332 pub fn with_decomposition(self, decomposition: BaseQuantityVector) -> 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_quantity_factors(
342 self,
343 derived_quantity_factors: Vec<(String, i32)>,
344 ) -> Self {
345 Self {
346 derived_quantity_factors,
347 ..self
348 }
349 }
350}
351
352impl Default for QuantityUnits {
353 fn default() -> Self {
354 QuantityUnits::new()
355 }
356}
357
358impl From<Vec<QuantityUnit>> for QuantityUnits {
359 fn from(v: Vec<QuantityUnit>) -> Self {
360 QuantityUnits(v)
361 }
362}
363
364impl<'a> IntoIterator for &'a QuantityUnits {
365 type Item = &'a QuantityUnit;
366 type IntoIter = std::slice::Iter<'a, QuantityUnit>;
367 fn into_iter(self) -> Self::IntoIter {
368 self.0.iter()
369 }
370}
371
372#[derive(Clone, Debug, PartialEq, Eq, Hash)]
373pub struct RatioUnit {
374 pub name: String,
375 pub value: RationalInteger,
376 pub minimum: Option<RationalInteger>,
377 pub maximum: Option<RationalInteger>,
378 pub default_magnitude: Option<RationalInteger>,
379}
380
381impl Serialize for RatioUnit {
382 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
383 use quantity_unit_factor_serialization::FactorSerializer;
384 use serde::ser::SerializeStruct;
385 let mut state = serializer.serialize_struct("RatioUnit", 5)?;
386 state.serialize_field("name", &self.name)?;
387 state.serialize_field("value", &FactorSerializer::from_ratio(&self.value))?;
388 if let Some(minimum) = &self.minimum {
389 state.serialize_field(
390 "minimum",
391 &rational_to_serialized_str(minimum)
392 .expect("BUG: planned ratio unit minimum must serialize to decimal string"),
393 )?;
394 }
395 if let Some(maximum) = &self.maximum {
396 state.serialize_field(
397 "maximum",
398 &rational_to_serialized_str(maximum)
399 .expect("BUG: planned ratio unit maximum must serialize to decimal string"),
400 )?;
401 }
402 if let Some(default_magnitude) = &self.default_magnitude {
403 state.serialize_field(
404 "default",
405 &rational_to_serialized_str(default_magnitude)
406 .expect("BUG: planned ratio unit default must serialize to decimal string"),
407 )?;
408 }
409 state.end()
410 }
411}
412
413impl<'de> Deserialize<'de> for RatioUnit {
414 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
415 #[derive(Deserialize)]
416 struct RatioUnitData {
417 name: String,
418 #[serde(with = "quantity_unit_factor_serialization")]
419 value: RationalInteger,
420 #[serde(default)]
421 minimum: Option<Decimal>,
422 #[serde(default)]
423 maximum: Option<Decimal>,
424 #[serde(default, rename = "default")]
425 default_magnitude: Option<Decimal>,
426 }
427 let data = RatioUnitData::deserialize(deserializer)?;
428 Ok(Self {
429 name: data.name,
430 value: data.value,
431 minimum: data
432 .minimum
433 .map(rational_from_parsed_decimal)
434 .transpose()
435 .map_err(serde::de::Error::custom)?,
436 maximum: data
437 .maximum
438 .map(rational_from_parsed_decimal)
439 .transpose()
440 .map_err(serde::de::Error::custom)?,
441 default_magnitude: data
442 .default_magnitude
443 .map(rational_from_parsed_decimal)
444 .transpose()
445 .map_err(serde::de::Error::custom)?,
446 })
447 }
448}
449
450impl RatioUnit {
451 pub fn clear_constraint_magnitudes(&mut self) {
452 self.minimum = None;
453 self.maximum = None;
454 self.default_magnitude = None;
455 }
456
457 pub fn value_decimal(&self) -> Decimal {
459 rational::commit_rational_to_decimal(&self.value)
460 .expect("BUG: ratio unit value must commit to decimal")
461 }
462
463 #[must_use]
464 pub fn minimum_decimal(&self) -> Option<Decimal> {
465 self.minimum.as_ref().map(|bound| {
466 rational::commit_rational_to_decimal(bound)
467 .expect("BUG: planned ratio unit minimum must commit to decimal")
468 })
469 }
470
471 #[must_use]
472 pub fn maximum_decimal(&self) -> Option<Decimal> {
473 self.maximum.as_ref().map(|bound| {
474 rational::commit_rational_to_decimal(bound)
475 .expect("BUG: planned ratio unit maximum must commit to decimal")
476 })
477 }
478
479 #[must_use]
480 pub fn default_magnitude_decimal(&self) -> Option<Decimal> {
481 self.default_magnitude.as_ref().map(|bound| {
482 rational::commit_rational_to_decimal(bound)
483 .expect("BUG: planned ratio unit default must commit to decimal")
484 })
485 }
486
487 #[must_use]
489 pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
490 self.maximum.as_ref().map(|maximum| {
491 let canonical = rational::checked_mul(maximum, &self.value)
492 .expect("BUG: planned ratio unit maximum canonical multiply must succeed");
493 rational::commit_rational_to_decimal(&canonical)
494 .expect("BUG: planned ratio unit maximum canonical must commit to decimal")
495 })
496 }
497}
498
499#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
500#[serde(transparent)]
501pub struct RatioUnits(pub Vec<RatioUnit>);
502
503impl RatioUnits {
504 pub fn new() -> Self {
505 RatioUnits(Vec::new())
506 }
507 pub fn get(&self, name: &str) -> Result<&RatioUnit, String> {
508 self.0.iter().find(|u| u.name == name).ok_or_else(|| {
509 let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
510 format!(
511 "Unknown unit '{}' for this ratio type. Valid units: {}",
512 name,
513 valid.join(", ")
514 )
515 })
516 }
517
518 pub fn iter(&self) -> std::slice::Iter<'_, RatioUnit> {
519 self.0.iter()
520 }
521 pub fn push(&mut self, u: RatioUnit) {
522 self.0.push(u);
523 }
524 pub fn is_empty(&self) -> bool {
525 self.0.is_empty()
526 }
527 pub fn len(&self) -> usize {
528 self.0.len()
529 }
530}
531
532impl Default for RatioUnits {
533 fn default() -> Self {
534 RatioUnits::new()
535 }
536}
537
538impl From<Vec<RatioUnit>> for RatioUnits {
539 fn from(v: Vec<RatioUnit>) -> Self {
540 RatioUnits(v)
541 }
542}
543
544impl<'a> IntoIterator for &'a RatioUnits {
545 type Item = &'a RatioUnit;
546 type IntoIter = std::slice::Iter<'a, RatioUnit>;
547 fn into_iter(self) -> Self::IntoIter {
548 self.0.iter()
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
557#[serde(rename_all = "lowercase")]
558pub enum BooleanValue {
559 True,
560 False,
561 Yes,
562 No,
563 Accept,
564 Reject,
565}
566
567impl From<BooleanValue> for bool {
568 fn from(value: BooleanValue) -> bool {
569 matches!(
570 value,
571 BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept
572 )
573 }
574}
575
576impl From<&BooleanValue> for bool {
577 fn from(value: &BooleanValue) -> bool {
578 (*value).into() }
580}
581
582impl From<bool> for BooleanValue {
583 fn from(value: bool) -> BooleanValue {
584 if value {
585 BooleanValue::True
586 } else {
587 BooleanValue::False
588 }
589 }
590}
591
592impl std::ops::Not for BooleanValue {
593 type Output = BooleanValue;
594
595 fn not(self) -> Self::Output {
596 if self.into() {
597 BooleanValue::False
598 } else {
599 BooleanValue::True
600 }
601 }
602}
603
604impl std::ops::Not for &BooleanValue {
605 type Output = BooleanValue;
606
607 fn not(self) -> Self::Output {
608 if (*self).into() {
609 BooleanValue::False
610 } else {
611 BooleanValue::True
612 }
613 }
614}
615
616impl std::str::FromStr for BooleanValue {
617 type Err = String;
618
619 fn from_str(s: &str) -> Result<Self, Self::Err> {
620 match s.trim().to_lowercase().as_str() {
621 "true" => Ok(BooleanValue::True),
622 "false" => Ok(BooleanValue::False),
623 "yes" => Ok(BooleanValue::Yes),
624 "no" => Ok(BooleanValue::No),
625 "accept" => Ok(BooleanValue::Accept),
626 "reject" => Ok(BooleanValue::Reject),
627 _ => Err(format!("Invalid boolean: '{}'", s)),
628 }
629 }
630}
631
632impl BooleanValue {
633 #[must_use]
634 pub fn as_str(&self) -> &'static str {
635 match self {
636 BooleanValue::True => "true",
637 BooleanValue::False => "false",
638 BooleanValue::Yes => "yes",
639 BooleanValue::No => "no",
640 BooleanValue::Accept => "accept",
641 BooleanValue::Reject => "reject",
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 hours = self.offset_hours.abs();
665 write!(f, "{}{:02}:{:02}", sign, hours, 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);
1077
1078impl std::str::FromStr for NumberLiteral {
1079 type Err = String;
1080
1081 fn from_str(s: &str) -> Result<Self, Self::Err> {
1082 let clean = s.trim().replace(['_', ','], "");
1083 let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
1084 if digit_count > crate::limits::MAX_NUMBER_DIGITS {
1085 return Err(format!(
1086 "Number has too many digits (max {})",
1087 crate::limits::MAX_NUMBER_DIGITS
1088 ));
1089 }
1090 Decimal::from_str(&clean)
1091 .map_err(|_| format!("Invalid number: '{}'", s))
1092 .map(NumberLiteral)
1093 }
1094}
1095
1096pub(crate) struct TextLiteral(pub String);
1098
1099impl std::str::FromStr for TextLiteral {
1100 type Err = String;
1101
1102 fn from_str(s: &str) -> Result<Self, Self::Err> {
1103 if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
1104 return Err(format!(
1105 "Text value exceeds maximum length (max {} characters)",
1106 crate::limits::MAX_TEXT_VALUE_LENGTH
1107 ));
1108 }
1109 Ok(TextLiteral(s.to_string()))
1110 }
1111}
1112
1113pub(crate) struct NumberWithUnit(pub Decimal, pub String);
1115
1116impl std::str::FromStr for NumberWithUnit {
1117 type Err = String;
1118
1119 fn from_str(s: &str) -> Result<Self, Self::Err> {
1120 let trimmed = s.trim();
1121 if trimmed.is_empty() {
1122 return Err(
1123 "Quantity value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
1124 .to_string(),
1125 );
1126 }
1127
1128 let mut parts = trimmed.split_whitespace();
1129 let number_part = parts
1130 .next()
1131 .expect("split_whitespace yields >=1 token after non-empty guard");
1132 let unit_part = parts.next().ok_or_else(|| {
1133 format!(
1134 "Quantity value must include a unit (e.g. '{} eur').",
1135 number_part
1136 )
1137 })?;
1138 if parts.next().is_some() {
1139 return Err(format!(
1140 "Invalid quantity value: '{}'. Expected exactly '<number> <unit>', got extra tokens.",
1141 s
1142 ));
1143 }
1144 let n = number_part
1145 .parse::<NumberLiteral>()
1146 .map_err(|_| format!("Invalid quantity: '{}'", s))?
1147 .0;
1148 Ok(NumberWithUnit(n, unit_part.to_string()))
1149 }
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Eq)]
1177pub(crate) enum RatioLiteral {
1178 Bare(Decimal),
1179 Percent(Decimal),
1180 Permille(Decimal),
1181 Named { value: Decimal, unit: String },
1182}
1183
1184impl std::str::FromStr for RatioLiteral {
1185 type Err = String;
1186
1187 fn from_str(s: &str) -> Result<Self, Self::Err> {
1188 let trimmed = s.trim();
1189 if trimmed.is_empty() {
1190 return Err(
1191 "Ratio value cannot be empty. Use a number, optionally followed by '%', '%%', or a unit name (e.g. '0.5', '50%', '25%%', '50 percent')."
1192 .to_string(),
1193 );
1194 }
1195
1196 let mut parts = trimmed.split_whitespace();
1197 let first = parts
1198 .next()
1199 .expect("split_whitespace yields >=1 token after non-empty guard");
1200 let second = parts.next();
1201 if parts.next().is_some() {
1202 return Err(format!(
1203 "Invalid ratio value: '{}'. Expected '<number>', '<number>%', '<number>%%', or '<number> <unit>'.",
1204 s
1205 ));
1206 }
1207
1208 match second {
1209 None => {
1211 if let Some(rest) = first.strip_suffix("%%") {
1212 if rest.is_empty() {
1213 return Err(format!(
1214 "Invalid ratio value: '{}'. '%%' must follow a number (e.g. '25%%').",
1215 s
1216 ));
1217 }
1218 let n = rest
1219 .parse::<NumberLiteral>()
1220 .map_err(|_| {
1221 format!(
1222 "Invalid ratio value: '{}'. '{}' is not a valid number before '%%'.",
1223 s, rest
1224 )
1225 })?
1226 .0;
1227 return Ok(RatioLiteral::Permille(n));
1228 }
1229 if let Some(rest) = first.strip_suffix('%') {
1230 if rest.is_empty() {
1231 return Err(format!(
1232 "Invalid ratio value: '{}'. '%' must follow a number (e.g. '50%').",
1233 s
1234 ));
1235 }
1236 let n = rest
1237 .parse::<NumberLiteral>()
1238 .map_err(|_| {
1239 format!(
1240 "Invalid ratio value: '{}'. '{}' is not a valid number before '%'.",
1241 s, rest
1242 )
1243 })?
1244 .0;
1245 return Ok(RatioLiteral::Percent(n));
1246 }
1247 let n = first.parse::<NumberLiteral>().map_err(|_| {
1248 format!(
1249 "Invalid ratio value: '{}'. Must be a number, '<n>%', '<n>%%', '<n> percent', '<n> permille', or '<n> <unit>'.",
1250 s
1251 )
1252 })?.0;
1253 Ok(RatioLiteral::Bare(n))
1254 }
1255 Some(unit) => {
1257 if unit == "%" || unit == "%%" {
1258 return Err(format!(
1259 "Invalid ratio value: '{}'. '{}' must be glued to the number (e.g. '{}{}'), not separated by whitespace.",
1260 s, unit, first, unit
1261 ));
1262 }
1263 let n = first
1264 .parse::<NumberLiteral>()
1265 .map_err(|_| {
1266 format!(
1267 "Invalid ratio value: '{}'. '{}' is not a valid number.",
1268 s, first
1269 )
1270 })?
1271 .0;
1272 Ok(RatioLiteral::Named {
1273 value: n,
1274 unit: unit.to_string(),
1275 })
1276 }
1277 }
1278 }
1279}