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