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