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<crate::planning::semantics::Dimension, 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)]
415pub struct TimezoneValue {
416 pub offset_minutes: i16,
417}
418
419const MAX_TIMEZONE_OFFSET_MINUTES: i16 = 23 * 60 + 59;
420
421impl TimezoneValue {
422 pub fn from_offset_seconds(seconds: i32) -> Option<Self> {
424 if seconds % 60 != 0 {
425 return None;
426 }
427 let offset_minutes = i16::try_from(seconds / 60).ok()?;
428 (offset_minutes.unsigned_abs() <= MAX_TIMEZONE_OFFSET_MINUTES.unsigned_abs())
429 .then_some(Self { offset_minutes })
430 }
431
432 pub fn offset_seconds(&self) -> i32 {
433 i32::from(self.offset_minutes) * 60
434 }
435}
436
437impl fmt::Display for TimezoneValue {
438 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439 if self.offset_minutes == 0 {
440 return write!(f, "Z");
441 }
442 let sign = if self.offset_minutes > 0 { "+" } else { "-" };
443 let magnitude = self.offset_minutes.unsigned_abs();
444 write!(f, "{}{:02}:{:02}", sign, magnitude / 60, magnitude % 60)
445 }
446}
447
448impl Serialize for TimezoneValue {
449 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
450 serializer.serialize_str(&self.to_string())
451 }
452}
453
454impl<'de> Deserialize<'de> for TimezoneValue {
455 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
456 let s = String::deserialize(deserializer)?;
457 Self::from_str(&s).map_err(serde::de::Error::custom)
458 }
459}
460
461impl FromStr for TimezoneValue {
462 type Err = String;
463
464 fn from_str(s: &str) -> Result<Self, Self::Err> {
465 let trimmed = s.trim();
466 if trimmed == "Z" || trimmed == "z" {
467 return Ok(Self { offset_minutes: 0 });
468 }
469 if trimmed.len() == 6
470 && (trimmed.starts_with('+') || trimmed.starts_with('-'))
471 && trimmed.as_bytes()[3] == b':'
472 {
473 let hours: i16 = trimmed[1..3]
474 .parse()
475 .map_err(|_| format!("Invalid timezone format: '{s}'"))?;
476 let minutes: i16 = trimmed[4..6]
477 .parse()
478 .map_err(|_| format!("Invalid timezone format: '{s}'"))?;
479 if !(0..=23).contains(&hours) || !(0..60).contains(&minutes) {
480 return Err(format!("Invalid timezone format: '{s}'"));
481 }
482 let magnitude = hours * 60 + minutes;
483 let offset_minutes = if trimmed.starts_with('-') {
484 -magnitude
485 } else {
486 magnitude
487 };
488 return Ok(Self { offset_minutes });
489 }
490 Err(format!("Invalid timezone format: '{s}'"))
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
495pub struct TimeValue {
496 pub hour: u8,
497 pub minute: u8,
498 pub second: u8,
499 pub microsecond: u32,
500 pub timezone: Option<TimezoneValue>,
501}
502
503impl Serialize for TimeValue {
504 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
505 serializer.serialize_str(&self.to_string())
506 }
507}
508
509impl<'de> Deserialize<'de> for TimeValue {
510 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
511 let s = String::deserialize(deserializer)?;
512 Self::from_str(&s).map_err(serde::de::Error::custom)
513 }
514}
515
516impl fmt::Display for TimeValue {
517 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
518 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
519 if self.microsecond != 0 {
520 write!(f, ".{:06}", self.microsecond)?;
521 }
522 if let Some(timezone) = &self.timezone {
523 write!(f, "{}", timezone)?;
524 }
525 Ok(())
526 }
527}
528
529#[derive(
530 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
531)]
532#[serde(rename_all = "snake_case")]
533pub enum DateGranularity {
534 Year,
535 YearMonth,
536 IsoWeek {
540 iso_year: i32,
541 week: u32,
542 },
543 #[default]
544 Full,
545 DateTime,
546}
547
548#[derive(Debug, Clone)]
549pub struct DateTimeValue {
550 pub year: i32,
551 pub month: u32,
552 pub day: u32,
553 pub hour: u32,
554 pub minute: u32,
555 pub second: u32,
556 pub microsecond: u32,
557 pub timezone: Option<TimezoneValue>,
558 pub granularity: DateGranularity,
559}
560
561impl Serialize for DateTimeValue {
562 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
563 serializer.serialize_str(&self.to_string())
564 }
565}
566
567impl<'de> Deserialize<'de> for DateTimeValue {
568 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
569 let s = String::deserialize(deserializer)?;
570 Self::from_str(&s).map_err(serde::de::Error::custom)
571 }
572}
573
574impl PartialEq for DateTimeValue {
575 fn eq(&self, other: &Self) -> bool {
576 self.year == other.year
577 && self.month == other.month
578 && self.day == other.day
579 && self.hour == other.hour
580 && self.minute == other.minute
581 && self.second == other.second
582 && self.microsecond == other.microsecond
583 && self.timezone == other.timezone
584 }
585}
586
587impl Eq for DateTimeValue {}
588
589impl PartialOrd for DateTimeValue {
590 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
591 Some(self.cmp(other))
592 }
593}
594
595impl Ord for DateTimeValue {
596 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
597 self.year
598 .cmp(&other.year)
599 .then_with(|| self.month.cmp(&other.month))
600 .then_with(|| self.day.cmp(&other.day))
601 .then_with(|| self.hour.cmp(&other.hour))
602 .then_with(|| self.minute.cmp(&other.minute))
603 .then_with(|| self.second.cmp(&other.second))
604 .then_with(|| self.microsecond.cmp(&other.microsecond))
605 .then_with(|| self.timezone.cmp(&other.timezone))
606 }
607}
608
609impl std::hash::Hash for DateTimeValue {
610 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
611 self.year.hash(state);
612 self.month.hash(state);
613 self.day.hash(state);
614 self.hour.hash(state);
615 self.minute.hash(state);
616 self.second.hash(state);
617 self.microsecond.hash(state);
618 self.timezone.hash(state);
619 }
620}
621
622impl DateTimeValue {
623 pub fn now() -> Self {
624 let now = chrono::Local::now();
625 let offset_secs = now.offset().local_minus_utc();
626 Self {
627 year: now.year(),
628 month: now.month(),
629 day: now.day(),
630 hour: now.time().hour(),
631 minute: now.time().minute(),
632 second: now.time().second(),
633 microsecond: now.time().nanosecond() / 1000 % 1_000_000,
634 timezone: Some(TimezoneValue::from_offset_seconds(offset_secs).expect(
635 "BUG: current local UTC offsets are whole minutes within ±23:59 since 1972",
636 )),
637 granularity: DateGranularity::DateTime,
638 }
639 }
640
641 fn parse_iso_week(s: &str) -> Option<Self> {
642 let parts: Vec<&str> = s.split("-W").collect();
643 if parts.len() != 2 {
644 return None;
645 }
646 let iso_year: i32 = parts[0].parse().ok()?;
647 let week: u32 = parts[1].parse().ok()?;
648 if week == 0 || week > 53 {
649 return None;
650 }
651 let date = chrono::NaiveDate::from_isoywd_opt(iso_year, week, chrono::Weekday::Mon)?;
652 Some(Self {
653 year: date.year(),
654 month: date.month(),
655 day: date.day(),
656 hour: 0,
657 minute: 0,
658 second: 0,
659 microsecond: 0,
660 timezone: None,
661 granularity: DateGranularity::IsoWeek { iso_year, week },
662 })
663 }
664}
665
666impl fmt::Display for DateTimeValue {
667 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668 match self.granularity {
669 DateGranularity::Year => write!(f, "{:04}", self.year),
670 DateGranularity::YearMonth => write!(f, "{:04}-{:02}", self.year, self.month),
671 DateGranularity::IsoWeek { iso_year, week } => {
672 write!(f, "{:04}-W{:02}", iso_year, week)
673 }
674 DateGranularity::Full => {
675 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
676 }
677 DateGranularity::DateTime => {
678 write!(
679 f,
680 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
681 self.year, self.month, self.day, self.hour, self.minute, self.second
682 )?;
683 if self.microsecond != 0 {
684 write!(f, ".{:06}", self.microsecond)?;
685 }
686 if let Some(tz) = &self.timezone {
687 write!(f, "{}", tz)?;
688 }
689 Ok(())
690 }
691 }
692 }
693}
694
695pub mod decimal_binary_serde {
700 use rust_decimal::Decimal;
701 use serde::{Deserialize, Deserializer, Serialize, Serializer};
702
703 pub fn serialize<S: Serializer>(value: &Decimal, serializer: S) -> Result<S::Ok, S::Error> {
704 (value.mantissa(), value.scale()).serialize(serializer)
705 }
706
707 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Decimal, D::Error> {
708 let (mantissa, scale): (i128, u32) = Deserialize::deserialize(deserializer)?;
709 Decimal::try_from_i128_with_scale(mantissa, scale).map_err(serde::de::Error::custom)
710 }
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
718#[serde(rename_all = "snake_case")]
719pub enum Value {
720 Number(#[serde(with = "decimal_binary_serde")] Decimal),
721 NumberWithUnit(#[serde(with = "decimal_binary_serde")] Decimal, String),
722 Text(String),
723 Date(DateTimeValue),
724 Time(TimeValue),
725 Boolean(BooleanValue),
726 Range(Box<Value>, Box<Value>),
727}
728
729impl fmt::Display for Value {
730 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731 match self {
732 Value::Number(n) => write!(f, "{}", n),
733 Value::Text(s) => write!(f, "{}", s),
734 Value::Date(dt) => write!(f, "{}", dt),
735 Value::Boolean(b) => write!(f, "{}", b),
736 Value::Time(time) => write!(f, "{}", time),
737 Value::NumberWithUnit(n, u) => match u.as_str() {
738 "percent" => {
739 let norm = n.normalize();
740 let s = if norm.fract().is_zero() {
741 norm.trunc().to_string()
742 } else {
743 norm.to_string()
744 };
745 write!(f, "{}%", s)
746 }
747 "permille" => {
748 let norm = n.normalize();
749 let s = if norm.fract().is_zero() {
750 norm.trunc().to_string()
751 } else {
752 norm.to_string()
753 };
754 write!(f, "{}%%", s)
755 }
756 unit => {
757 let norm = n.normalize();
758 let s = if norm.fract().is_zero() {
759 norm.trunc().to_string()
760 } else {
761 norm.to_string()
762 };
763 write!(f, "{} {}", s, unit)
764 }
765 },
766 Value::Range(left, right) => write!(f, "{}...{}", left, right),
767 }
768 }
769}
770
771impl std::str::FromStr for DateTimeValue {
776 type Err = String;
777
778 fn from_str(s: &str) -> Result<Self, Self::Err> {
779 if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
780 let timezone = TimezoneValue::from_offset_seconds(dt.offset().local_minus_utc())
781 .ok_or_else(|| format!("Invalid timezone offset in date: '{}'", s))?;
782 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
783 return Ok(DateTimeValue {
784 year: dt.year(),
785 month: dt.month(),
786 day: dt.day(),
787 hour: dt.hour(),
788 minute: dt.minute(),
789 second: dt.second(),
790 microsecond,
791 timezone: Some(timezone),
792 granularity: DateGranularity::DateTime,
793 });
794 }
795 if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
796 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
797 return Ok(DateTimeValue {
798 year: dt.year(),
799 month: dt.month(),
800 day: dt.day(),
801 hour: dt.hour(),
802 minute: dt.minute(),
803 second: dt.second(),
804 microsecond,
805 timezone: None,
806 granularity: DateGranularity::DateTime,
807 });
808 }
809 if let Ok(d) = s.parse::<chrono::NaiveDate>() {
810 return Ok(DateTimeValue {
811 year: d.year(),
812 month: d.month(),
813 day: d.day(),
814 hour: 0,
815 minute: 0,
816 second: 0,
817 microsecond: 0,
818 timezone: None,
819 granularity: DateGranularity::Full,
820 });
821 }
822 if let Some(week_val) = Self::parse_iso_week(s) {
823 return Ok(week_val);
824 }
825 if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
826 return Ok(Self {
827 year: ym.year(),
828 month: ym.month(),
829 day: 1,
830 hour: 0,
831 minute: 0,
832 second: 0,
833 microsecond: 0,
834 timezone: None,
835 granularity: DateGranularity::YearMonth,
836 });
837 }
838 if let Ok(year) = s.parse::<i32>() {
839 if (1..=9999).contains(&year) {
840 return Ok(Self {
841 year,
842 month: 1,
843 day: 1,
844 hour: 0,
845 minute: 0,
846 second: 0,
847 microsecond: 0,
848 timezone: None,
849 granularity: DateGranularity::Year,
850 });
851 }
852 }
853 Err(format!("Invalid date format: '{}'", s))
854 }
855}
856
857impl std::str::FromStr for TimeValue {
858 type Err = String;
859
860 fn from_str(s: &str) -> Result<Self, Self::Err> {
861 let trimmed = s.trim();
862
863 let (time_text, timezone) = if trimmed.ends_with('Z') || trimmed.ends_with('z') {
864 (
865 &trimmed[..trimmed.len() - 1],
866 Some(TimezoneValue { offset_minutes: 0 }),
867 )
868 } else if let Some(first) = trimmed.chars().next() {
869 let after_first = first.len_utf8();
870 if let Some(sign_index) = trimmed[after_first..]
871 .rfind(['+', '-'])
872 .map(|index| index + after_first)
873 {
874 let timezone_text = &trimmed[sign_index..];
875 if timezone_text.len() == 6
876 && (timezone_text.starts_with('+') || timezone_text.starts_with('-'))
877 && timezone_text.as_bytes()[3] == b':'
878 {
879 let timezone = TimezoneValue::from_str(timezone_text)
880 .map_err(|_| format!("Invalid time format: '{s}'"))?;
881 (&trimmed[..sign_index], Some(timezone))
882 } else {
883 (trimmed, None)
884 }
885 } else {
886 (trimmed, None)
887 }
888 } else {
889 (trimmed, None)
890 };
891
892 if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S%.f") {
893 return Ok(TimeValue {
894 hour: t.hour() as u8,
895 minute: t.minute() as u8,
896 second: t.second() as u8,
897 microsecond: t.nanosecond() / 1000 % 1_000_000,
898 timezone,
899 });
900 }
901 if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S") {
902 return Ok(TimeValue {
903 hour: t.hour() as u8,
904 minute: t.minute() as u8,
905 second: t.second() as u8,
906 microsecond: 0,
907 timezone,
908 });
909 }
910 if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M") {
911 return Ok(TimeValue {
912 hour: t.hour() as u8,
913 minute: t.minute() as u8,
914 second: 0,
915 microsecond: 0,
916 timezone,
917 });
918 }
919 Err(format!("Invalid time format: '{}'", s))
920 }
921}
922
923pub(crate) struct NumberLiteral(pub Decimal);
928
929impl std::str::FromStr for NumberLiteral {
930 type Err = String;
931
932 fn from_str(s: &str) -> Result<Self, Self::Err> {
933 let clean = s.trim().replace('_', "");
934 if let Some(dot) = clean.find('.') {
935 let frac = &clean[dot + 1..];
936 let frac_digits = frac.chars().take_while(|c| c.is_ascii_digit()).count();
937 if frac_digits > Decimal::MAX_SCALE as usize {
938 return Err(format!(
939 "Invalid number '{}': too many fractional digits (max {})",
940 s,
941 Decimal::MAX_SCALE
942 ));
943 }
944 }
945 Decimal::from_str(&clean)
946 .map_err(|_| format!("Invalid number: '{}'", s))
947 .map(NumberLiteral)
948 }
949}
950
951pub(crate) struct TextLiteral(pub String);
953
954impl std::str::FromStr for TextLiteral {
955 type Err = String;
956
957 fn from_str(s: &str) -> Result<Self, Self::Err> {
958 if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
959 return Err(format!(
960 "Text value exceeds maximum length (max {} characters)",
961 crate::limits::MAX_TEXT_VALUE_LENGTH
962 ));
963 }
964 Ok(TextLiteral(s.to_string()))
965 }
966}
967
968pub(crate) struct NumberWithUnit(pub Decimal, pub String);
970
971impl std::str::FromStr for NumberWithUnit {
972 type Err = String;
973
974 fn from_str(s: &str) -> Result<Self, Self::Err> {
975 let trimmed = s.trim();
976 if trimmed.is_empty() {
977 return Err(
978 "Measure value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
979 .to_string(),
980 );
981 }
982
983 let mut parts = trimmed.split_whitespace();
984 let number_part = parts
985 .next()
986 .expect("split_whitespace yields >=1 token after non-empty guard");
987 let unit_part = parts.next().ok_or_else(|| {
988 format!(
989 "Measure value must include a unit (e.g. '{} eur').",
990 number_part
991 )
992 })?;
993 if parts.next().is_some() {
994 return Err(format!(
995 "Invalid measure value: '{}'. Expected exactly '<number> <unit>', got extra tokens.",
996 s
997 ));
998 }
999 let n = number_part
1000 .parse::<NumberLiteral>()
1001 .map_err(|_| format!("Invalid measure: '{}'", s))?
1002 .0;
1003 Ok(NumberWithUnit(n, unit_part.to_string()))
1004 }
1005}
1006
1007#[derive(Debug, Clone, PartialEq, Eq)]
1032pub(crate) enum RatioLiteral {
1033 Bare(Decimal),
1034 Percent(Decimal),
1035 Permille(Decimal),
1036 Named { value: Decimal, unit: String },
1037}
1038
1039impl std::str::FromStr for RatioLiteral {
1040 type Err = String;
1041
1042 fn from_str(s: &str) -> Result<Self, Self::Err> {
1043 let trimmed = s.trim();
1044 if trimmed.is_empty() {
1045 return Err(
1046 "Ratio value cannot be empty. Use a number, optionally followed by '%', '%%', or a unit name (e.g. '0.5', '50%', '25%%', '50 percent')."
1047 .to_string(),
1048 );
1049 }
1050
1051 let mut parts = trimmed.split_whitespace();
1052 let first = parts
1053 .next()
1054 .expect("split_whitespace yields >=1 token after non-empty guard");
1055 let second = parts.next();
1056 if parts.next().is_some() {
1057 return Err(format!(
1058 "Invalid ratio value: '{}'. Expected '<number>', '<number>%', '<number>%%', or '<number> <unit>'.",
1059 s
1060 ));
1061 }
1062
1063 match second {
1064 None => {
1066 if let Some(rest) = first.strip_suffix("%%") {
1067 if rest.is_empty() {
1068 return Err(format!(
1069 "Invalid ratio value: '{}'. '%%' must follow a number (e.g. '25%%').",
1070 s
1071 ));
1072 }
1073 let n = rest
1074 .parse::<NumberLiteral>()
1075 .map_err(|_| {
1076 format!(
1077 "Invalid ratio value: '{}'. '{}' is not a valid number before '%%'.",
1078 s, rest
1079 )
1080 })?
1081 .0;
1082 return Ok(RatioLiteral::Permille(n));
1083 }
1084 if let Some(rest) = first.strip_suffix('%') {
1085 if rest.is_empty() {
1086 return Err(format!(
1087 "Invalid ratio value: '{}'. '%' must follow a number (e.g. '50%').",
1088 s
1089 ));
1090 }
1091 let n = rest
1092 .parse::<NumberLiteral>()
1093 .map_err(|_| {
1094 format!(
1095 "Invalid ratio value: '{}'. '{}' is not a valid number before '%'.",
1096 s, rest
1097 )
1098 })?
1099 .0;
1100 return Ok(RatioLiteral::Percent(n));
1101 }
1102 let n = first.parse::<NumberLiteral>().map_err(|_| {
1103 format!(
1104 "Invalid ratio value: '{}'. Must be a number, '<n>%', '<n>%%', '<n> percent', '<n> permille', or '<n> <unit>'.",
1105 s
1106 )
1107 })?.0;
1108 Ok(RatioLiteral::Bare(n))
1109 }
1110 Some(unit) => {
1112 if unit == "%" || unit == "%%" {
1113 return Err(format!(
1114 "Invalid ratio value: '{}'. '{}' must be glued to the number (e.g. '{}{}'), not separated by whitespace.",
1115 s, unit, first, unit
1116 ));
1117 }
1118 let n = first
1119 .parse::<NumberLiteral>()
1120 .map_err(|_| {
1121 format!(
1122 "Invalid ratio value: '{}'. '{}' is not a valid number.",
1123 s, first
1124 )
1125 })?
1126 .0;
1127 Ok(RatioLiteral::Named {
1128 value: n,
1129 unit: unit.to_string(),
1130 })
1131 }
1132 }
1133 }
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use super::{BooleanValue, TimeValue, TimezoneValue};
1139 use std::str::FromStr;
1140
1141 #[test]
1142 fn timezone_value_round_trips_every_signed_offset() {
1143 for text in [
1144 "-00:30", "-03:30", "-09:30", "+05:45", "+00:30", "-12:00", "+14:00",
1145 ] {
1146 let timezone = TimezoneValue::from_str(text).expect("valid offset");
1147 assert_eq!(timezone.to_string(), text);
1148 }
1149 assert_eq!(TimezoneValue::from_str("+00:00").unwrap().to_string(), "Z");
1150 assert_eq!(TimezoneValue::from_str("-00:00").unwrap().to_string(), "Z");
1151 }
1152
1153 #[test]
1154 fn timezone_value_orders_by_signed_offset() {
1155 let ordered = [
1156 "-09:30", "-03:30", "-03:00", "-00:30", "Z", "+00:30", "+05:45",
1157 ]
1158 .map(|text| TimezoneValue::from_str(text).expect("valid offset"));
1159 for pair in ordered.windows(2) {
1160 assert!(
1161 pair[0] < pair[1],
1162 "{} must order before {}",
1163 pair[0],
1164 pair[1]
1165 );
1166 }
1167 }
1168
1169 #[test]
1170 fn time_value_rejects_multibyte_first_char() {
1171 for invalid in ["é", "é10:00", "日12:00", "é10:00+01:00", "€+01:00"] {
1172 assert!(
1173 TimeValue::from_str(invalid).is_err(),
1174 "'{invalid}' must not parse as time"
1175 );
1176 }
1177 }
1178
1179 #[test]
1180 fn time_value_parses_timezone_after_ascii_time() {
1181 let time = TimeValue::from_str("10:30:00+01:00").expect("valid time with offset");
1182 assert_eq!(time.hour, 10);
1183 assert_eq!(time.minute, 30);
1184 let timezone = time.timezone.expect("offset parsed");
1185 assert_eq!(timezone.offset_minutes, 60);
1186 }
1187
1188 #[test]
1189 fn boolean_value_rejects_accept_and_reject_strings() {
1190 for invalid in ["accept", "reject"] {
1191 assert!(
1192 BooleanValue::from_str(invalid).is_err(),
1193 "'{invalid}' must not parse as boolean"
1194 );
1195 }
1196 }
1197}