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