1use chrono::{Datelike, Timelike};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct ScaleUnit {
15 pub name: String,
16 pub value: Decimal,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct ScaleUnits(pub Vec<ScaleUnit>);
22
23impl ScaleUnits {
24 pub fn new() -> Self {
25 ScaleUnits(Vec::new())
26 }
27 pub fn get(&self, name: &str) -> Result<&ScaleUnit, String> {
28 self.0.iter().find(|u| u.name == name).ok_or_else(|| {
29 let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
30 format!(
31 "Unknown unit '{}' for this scale type. Valid units: {}",
32 name,
33 valid.join(", ")
34 )
35 })
36 }
37 pub fn iter(&self) -> std::slice::Iter<'_, ScaleUnit> {
38 self.0.iter()
39 }
40 pub fn push(&mut self, u: ScaleUnit) {
41 self.0.push(u);
42 }
43 pub fn is_empty(&self) -> bool {
44 self.0.is_empty()
45 }
46 pub fn len(&self) -> usize {
47 self.0.len()
48 }
49}
50
51impl Default for ScaleUnits {
52 fn default() -> Self {
53 ScaleUnits::new()
54 }
55}
56
57impl From<Vec<ScaleUnit>> for ScaleUnits {
58 fn from(v: Vec<ScaleUnit>) -> Self {
59 ScaleUnits(v)
60 }
61}
62
63impl<'a> IntoIterator for &'a ScaleUnits {
64 type Item = &'a ScaleUnit;
65 type IntoIter = std::slice::Iter<'a, ScaleUnit>;
66 fn into_iter(self) -> Self::IntoIter {
67 self.0.iter()
68 }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub struct RatioUnit {
73 pub name: String,
74 pub value: Decimal,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
78#[serde(transparent)]
79pub struct RatioUnits(pub Vec<RatioUnit>);
80
81impl RatioUnits {
82 pub fn new() -> Self {
83 RatioUnits(Vec::new())
84 }
85 pub fn get(&self, name: &str) -> Result<&RatioUnit, String> {
86 self.0.iter().find(|u| u.name == name).ok_or_else(|| {
87 let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
88 format!(
89 "Unknown unit '{}' for this ratio type. Valid units: {}",
90 name,
91 valid.join(", ")
92 )
93 })
94 }
95 pub fn iter(&self) -> std::slice::Iter<'_, RatioUnit> {
96 self.0.iter()
97 }
98 pub fn push(&mut self, u: RatioUnit) {
99 self.0.push(u);
100 }
101 pub fn is_empty(&self) -> bool {
102 self.0.is_empty()
103 }
104 pub fn len(&self) -> usize {
105 self.0.len()
106 }
107}
108
109impl Default for RatioUnits {
110 fn default() -> Self {
111 RatioUnits::new()
112 }
113}
114
115impl From<Vec<RatioUnit>> for RatioUnits {
116 fn from(v: Vec<RatioUnit>) -> Self {
117 RatioUnits(v)
118 }
119}
120
121impl<'a> IntoIterator for &'a RatioUnits {
122 type Item = &'a RatioUnit;
123 type IntoIter = std::slice::Iter<'a, RatioUnit>;
124 fn into_iter(self) -> Self::IntoIter {
125 self.0.iter()
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(rename_all = "lowercase")]
135pub enum BooleanValue {
136 True,
137 False,
138 Yes,
139 No,
140 Accept,
141 Reject,
142}
143
144impl From<BooleanValue> for bool {
145 fn from(value: BooleanValue) -> bool {
146 matches!(
147 value,
148 BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept
149 )
150 }
151}
152
153impl From<&BooleanValue> for bool {
154 fn from(value: &BooleanValue) -> bool {
155 (*value).into() }
157}
158
159impl From<bool> for BooleanValue {
160 fn from(value: bool) -> BooleanValue {
161 if value {
162 BooleanValue::True
163 } else {
164 BooleanValue::False
165 }
166 }
167}
168
169impl std::ops::Not for BooleanValue {
170 type Output = BooleanValue;
171
172 fn not(self) -> Self::Output {
173 if self.into() {
174 BooleanValue::False
175 } else {
176 BooleanValue::True
177 }
178 }
179}
180
181impl std::ops::Not for &BooleanValue {
182 type Output = BooleanValue;
183
184 fn not(self) -> Self::Output {
185 if (*self).into() {
186 BooleanValue::False
187 } else {
188 BooleanValue::True
189 }
190 }
191}
192
193impl std::str::FromStr for BooleanValue {
194 type Err = String;
195
196 fn from_str(s: &str) -> Result<Self, Self::Err> {
197 match s.trim().to_lowercase().as_str() {
198 "true" => Ok(BooleanValue::True),
199 "false" => Ok(BooleanValue::False),
200 "yes" => Ok(BooleanValue::Yes),
201 "no" => Ok(BooleanValue::No),
202 "accept" => Ok(BooleanValue::Accept),
203 "reject" => Ok(BooleanValue::Reject),
204 _ => Err(format!("Invalid boolean: '{}'", s)),
205 }
206 }
207}
208
209impl BooleanValue {
210 #[must_use]
211 pub fn as_str(&self) -> &'static str {
212 match self {
213 BooleanValue::True => "true",
214 BooleanValue::False => "false",
215 BooleanValue::Yes => "yes",
216 BooleanValue::No => "no",
217 BooleanValue::Accept => "accept",
218 BooleanValue::Reject => "reject",
219 }
220 }
221}
222
223impl fmt::Display for BooleanValue {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 write!(f, "{}", self.as_str())
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Hash)]
230pub enum DurationUnit {
231 Year,
232 Month,
233 Week,
234 Day,
235 Hour,
236 Minute,
237 Second,
238 Millisecond,
239 Microsecond,
240}
241
242impl Serialize for DurationUnit {
243 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
244 where
245 S: serde::Serializer,
246 {
247 serializer.serialize_str(&self.to_string())
248 }
249}
250
251impl<'de> Deserialize<'de> for DurationUnit {
252 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253 where
254 D: serde::Deserializer<'de>,
255 {
256 let s = String::deserialize(deserializer)?;
257 s.parse().map_err(serde::de::Error::custom)
258 }
259}
260
261impl fmt::Display for DurationUnit {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 let s = match self {
264 DurationUnit::Year => "years",
265 DurationUnit::Month => "months",
266 DurationUnit::Week => "weeks",
267 DurationUnit::Day => "days",
268 DurationUnit::Hour => "hours",
269 DurationUnit::Minute => "minutes",
270 DurationUnit::Second => "seconds",
271 DurationUnit::Millisecond => "milliseconds",
272 DurationUnit::Microsecond => "microseconds",
273 };
274 write!(f, "{}", s)
275 }
276}
277
278impl std::str::FromStr for DurationUnit {
279 type Err = String;
280
281 fn from_str(s: &str) -> Result<Self, Self::Err> {
282 match s.trim().to_lowercase().as_str() {
283 "year" | "years" => Ok(DurationUnit::Year),
284 "month" | "months" => Ok(DurationUnit::Month),
285 "week" | "weeks" => Ok(DurationUnit::Week),
286 "day" | "days" => Ok(DurationUnit::Day),
287 "hour" | "hours" => Ok(DurationUnit::Hour),
288 "minute" | "minutes" => Ok(DurationUnit::Minute),
289 "second" | "seconds" => Ok(DurationUnit::Second),
290 "millisecond" | "milliseconds" => Ok(DurationUnit::Millisecond),
291 "microsecond" | "microseconds" => Ok(DurationUnit::Microsecond),
292 _ => Err(format!("Unknown duration unit: '{}'", s)),
293 }
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
298pub struct TimezoneValue {
299 pub offset_hours: i8,
300 pub offset_minutes: u8,
301}
302
303impl fmt::Display for TimezoneValue {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 if self.offset_hours == 0 && self.offset_minutes == 0 {
306 write!(f, "Z")
307 } else {
308 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
309 let hours = self.offset_hours.abs();
310 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
311 }
312 }
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
316pub struct TimeValue {
317 pub hour: u8,
318 pub minute: u8,
319 pub second: u8,
320 pub timezone: Option<TimezoneValue>,
321}
322
323impl fmt::Display for TimeValue {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
326 }
327}
328
329#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
330pub struct DateTimeValue {
331 pub year: i32,
332 pub month: u32,
333 pub day: u32,
334 pub hour: u32,
335 pub minute: u32,
336 pub second: u32,
337 #[serde(default)]
338 pub microsecond: u32,
339 pub timezone: Option<TimezoneValue>,
340}
341
342impl DateTimeValue {
343 pub fn now() -> Self {
344 let now = chrono::Local::now();
345 let offset_secs = now.offset().local_minus_utc();
346 Self {
347 year: now.year(),
348 month: now.month(),
349 day: now.day(),
350 hour: now.time().hour(),
351 minute: now.time().minute(),
352 second: now.time().second(),
353 microsecond: now.time().nanosecond() / 1000 % 1_000_000,
354 timezone: Some(TimezoneValue {
355 offset_hours: (offset_secs / 3600) as i8,
356 offset_minutes: ((offset_secs.abs() % 3600) / 60) as u8,
357 }),
358 }
359 }
360
361 fn parse_iso_week(s: &str) -> Option<Self> {
362 let parts: Vec<&str> = s.split("-W").collect();
363 if parts.len() != 2 {
364 return None;
365 }
366 let year: i32 = parts[0].parse().ok()?;
367 let week: u32 = parts[1].parse().ok()?;
368 if week == 0 || week > 53 {
369 return None;
370 }
371 let date = chrono::NaiveDate::from_isoywd_opt(year, week, chrono::Weekday::Mon)?;
372 Some(Self {
373 year: date.year(),
374 month: date.month(),
375 day: date.day(),
376 hour: 0,
377 minute: 0,
378 second: 0,
379 microsecond: 0,
380 timezone: None,
381 })
382 }
383}
384
385impl fmt::Display for DateTimeValue {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 let has_time = self.hour != 0
388 || self.minute != 0
389 || self.second != 0
390 || self.microsecond != 0
391 || self.timezone.is_some();
392 if !has_time {
393 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
394 } else {
395 write!(
396 f,
397 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
398 self.year, self.month, self.day, self.hour, self.minute, self.second
399 )?;
400 if self.microsecond != 0 {
401 write!(f, ".{:06}", self.microsecond)?;
402 }
403 if let Some(tz) = &self.timezone {
404 write!(f, "{}", tz)?;
405 }
406 Ok(())
407 }
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
413#[serde(rename_all = "snake_case")]
414pub enum Value {
415 Number(Decimal),
416 Scale(Decimal, String),
417 Text(String),
418 Date(DateTimeValue),
419 Time(TimeValue),
420 Boolean(BooleanValue),
421 Duration(Decimal, DurationUnit),
422 Ratio(Decimal, Option<String>),
423}
424
425impl fmt::Display for Value {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 match self {
428 Value::Number(n) => write!(f, "{}", n),
429 Value::Text(s) => write!(f, "{}", s),
430 Value::Date(dt) => write!(f, "{}", dt),
431 Value::Boolean(b) => write!(f, "{}", b),
432 Value::Time(time) => write!(f, "{}", time),
433 Value::Scale(n, u) => write!(f, "{} {}", n, u),
434 Value::Duration(n, u) => write!(f, "{} {}", n, u),
435 Value::Ratio(n, u) => match u.as_deref() {
436 Some("percent") => {
437 let display_value = *n * Decimal::from(100);
438 let norm = display_value.normalize();
439 let s = if norm.fract().is_zero() {
440 norm.trunc().to_string()
441 } else {
442 norm.to_string()
443 };
444 write!(f, "{}%", s)
445 }
446 Some("permille") => {
447 let display_value = *n * Decimal::from(1000);
448 let norm = display_value.normalize();
449 let s = if norm.fract().is_zero() {
450 norm.trunc().to_string()
451 } else {
452 norm.to_string()
453 };
454 write!(f, "{}%%", s)
455 }
456 Some(unit) => {
457 let norm = n.normalize();
458 let s = if norm.fract().is_zero() {
459 norm.trunc().to_string()
460 } else {
461 norm.to_string()
462 };
463 write!(f, "{} {}", s, unit)
464 }
465 None => {
466 let norm = n.normalize();
467 let s = if norm.fract().is_zero() {
468 norm.trunc().to_string()
469 } else {
470 norm.to_string()
471 };
472 write!(f, "{}", s)
473 }
474 },
475 }
476 }
477}
478
479impl std::str::FromStr for DateTimeValue {
484 type Err = String;
485
486 fn from_str(s: &str) -> Result<Self, Self::Err> {
487 if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
488 let offset = dt.offset().local_minus_utc();
489 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
490 return Ok(DateTimeValue {
491 year: dt.year(),
492 month: dt.month(),
493 day: dt.day(),
494 hour: dt.hour(),
495 minute: dt.minute(),
496 second: dt.second(),
497 microsecond,
498 timezone: Some(TimezoneValue {
499 offset_hours: (offset / 3600) as i8,
500 offset_minutes: ((offset.abs() % 3600) / 60) as u8,
501 }),
502 });
503 }
504 if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
505 let microsecond = dt.nanosecond() / 1000 % 1_000_000;
506 return Ok(DateTimeValue {
507 year: dt.year(),
508 month: dt.month(),
509 day: dt.day(),
510 hour: dt.hour(),
511 minute: dt.minute(),
512 second: dt.second(),
513 microsecond,
514 timezone: None,
515 });
516 }
517 if let Ok(d) = s.parse::<chrono::NaiveDate>() {
518 return Ok(DateTimeValue {
519 year: d.year(),
520 month: d.month(),
521 day: d.day(),
522 hour: 0,
523 minute: 0,
524 second: 0,
525 microsecond: 0,
526 timezone: None,
527 });
528 }
529 if let Some(week_val) = Self::parse_iso_week(s) {
530 return Ok(week_val);
531 }
532 if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
533 return Ok(Self {
534 year: ym.year(),
535 month: ym.month(),
536 day: 1,
537 hour: 0,
538 minute: 0,
539 second: 0,
540 microsecond: 0,
541 timezone: None,
542 });
543 }
544 if let Ok(year) = s.parse::<i32>() {
545 if (1..=9999).contains(&year) {
546 return Ok(Self {
547 year,
548 month: 1,
549 day: 1,
550 hour: 0,
551 minute: 0,
552 second: 0,
553 microsecond: 0,
554 timezone: None,
555 });
556 }
557 }
558 Err(format!("Invalid date format: '{}'", s))
559 }
560}
561
562impl std::str::FromStr for TimeValue {
563 type Err = String;
564
565 fn from_str(s: &str) -> Result<Self, Self::Err> {
566 if let Ok(t) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
567 let offset = t.offset().local_minus_utc();
568 return Ok(TimeValue {
569 hour: t.hour() as u8,
570 minute: t.minute() as u8,
571 second: t.second() as u8,
572 timezone: Some(TimezoneValue {
573 offset_hours: (offset / 3600) as i8,
574 offset_minutes: ((offset.abs() % 3600) / 60) as u8,
575 }),
576 });
577 }
578 if let Ok(t) = s.parse::<chrono::NaiveTime>() {
579 return Ok(TimeValue {
580 hour: t.hour() as u8,
581 minute: t.minute() as u8,
582 second: t.second() as u8,
583 timezone: None,
584 });
585 }
586 Err(format!("Invalid time format: '{}'", s))
587 }
588}
589
590pub(crate) struct NumberLiteral(pub Decimal);
592
593impl std::str::FromStr for NumberLiteral {
594 type Err = String;
595
596 fn from_str(s: &str) -> Result<Self, Self::Err> {
597 let clean = s.trim().replace(['_', ','], "");
598 let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
599 if digit_count > crate::limits::MAX_NUMBER_DIGITS {
600 return Err(format!(
601 "Number has too many digits (max {})",
602 crate::limits::MAX_NUMBER_DIGITS
603 ));
604 }
605 Decimal::from_str(&clean)
606 .map_err(|_| format!("Invalid number: '{}'", s))
607 .map(NumberLiteral)
608 }
609}
610
611pub(crate) struct TextLiteral(pub String);
613
614impl std::str::FromStr for TextLiteral {
615 type Err = String;
616
617 fn from_str(s: &str) -> Result<Self, Self::Err> {
618 if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
619 return Err(format!(
620 "Text value exceeds maximum length (max {} characters)",
621 crate::limits::MAX_TEXT_VALUE_LENGTH
622 ));
623 }
624 Ok(TextLiteral(s.to_string()))
625 }
626}
627
628pub(crate) struct DurationLiteral(pub Decimal, pub DurationUnit);
630
631impl std::str::FromStr for DurationLiteral {
632 type Err = String;
633
634 fn from_str(s: &str) -> Result<Self, Self::Err> {
635 let trimmed = s.trim();
636 let mut parts: Vec<&str> = trimmed.split_whitespace().collect();
637 if parts.len() < 2 {
638 return Err(format!(
639 "Invalid duration: '{}'. Expected format: <number> <unit> (e.g. 10 hours, 2 weeks)",
640 s
641 ));
642 }
643 let unit_str = parts.pop().unwrap();
644 let number_str = parts.join(" ");
645 let n = number_str
646 .parse::<NumberLiteral>()
647 .map_err(|_| format!("Invalid duration number: '{}'", number_str))?
648 .0;
649 let unit = unit_str.parse()?;
650 Ok(DurationLiteral(n, unit))
651 }
652}
653
654pub(crate) struct NumberWithUnit(pub Decimal, pub String);
660
661impl std::str::FromStr for NumberWithUnit {
662 type Err = String;
663
664 fn from_str(s: &str) -> Result<Self, Self::Err> {
665 let trimmed = s.trim();
666 if trimmed.is_empty() {
667 return Err(
668 "Scale value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
669 .to_string(),
670 );
671 }
672
673 let mut parts = trimmed.split_whitespace();
674 let number_part = parts
675 .next()
676 .expect("split_whitespace yields >=1 token after non-empty guard");
677 let unit_part = parts.next().ok_or_else(|| {
678 format!(
679 "Scale value must include a unit (e.g. '{} eur').",
680 number_part
681 )
682 })?;
683 if parts.next().is_some() {
684 return Err(format!(
685 "Invalid scale value: '{}'. Expected exactly '<number> <unit>', got extra tokens.",
686 s
687 ));
688 }
689 let n = number_part
690 .parse::<NumberLiteral>()
691 .map_err(|_| format!("Invalid scale: '{}'", s))?
692 .0;
693 Ok(NumberWithUnit(n, unit_part.to_string()))
694 }
695}
696
697#[derive(Debug, Clone, PartialEq, Eq)]
722pub(crate) enum RatioLiteral {
723 Bare(Decimal),
724 Percent(Decimal),
725 Permille(Decimal),
726 Named { value: Decimal, unit: String },
727}
728
729impl std::str::FromStr for RatioLiteral {
730 type Err = String;
731
732 fn from_str(s: &str) -> Result<Self, Self::Err> {
733 let trimmed = s.trim();
734 if trimmed.is_empty() {
735 return Err(
736 "Ratio value cannot be empty. Use a number, optionally followed by '%', '%%', or a unit name (e.g. '0.5', '50%', '25%%', '50 percent')."
737 .to_string(),
738 );
739 }
740
741 let mut parts = trimmed.split_whitespace();
742 let first = parts
743 .next()
744 .expect("split_whitespace yields >=1 token after non-empty guard");
745 let second = parts.next();
746 if parts.next().is_some() {
747 return Err(format!(
748 "Invalid ratio value: '{}'. Expected '<number>', '<number>%', '<number>%%', or '<number> <unit>'.",
749 s
750 ));
751 }
752
753 match second {
754 None => {
756 if let Some(rest) = first.strip_suffix("%%") {
757 if rest.is_empty() {
758 return Err(format!(
759 "Invalid ratio value: '{}'. '%%' must follow a number (e.g. '25%%').",
760 s
761 ));
762 }
763 let n = rest
764 .parse::<NumberLiteral>()
765 .map_err(|_| {
766 format!(
767 "Invalid ratio value: '{}'. '{}' is not a valid number before '%%'.",
768 s, rest
769 )
770 })?
771 .0;
772 return Ok(RatioLiteral::Permille(n / Decimal::from(1000)));
773 }
774 if let Some(rest) = first.strip_suffix('%') {
775 if rest.is_empty() {
776 return Err(format!(
777 "Invalid ratio value: '{}'. '%' must follow a number (e.g. '50%').",
778 s
779 ));
780 }
781 let n = rest
782 .parse::<NumberLiteral>()
783 .map_err(|_| {
784 format!(
785 "Invalid ratio value: '{}'. '{}' is not a valid number before '%'.",
786 s, rest
787 )
788 })?
789 .0;
790 return Ok(RatioLiteral::Percent(n / Decimal::from(100)));
791 }
792 let n = first.parse::<NumberLiteral>().map_err(|_| {
793 format!(
794 "Invalid ratio value: '{}'. Must be a number, '<n>%', '<n>%%', '<n> percent', '<n> permille', or '<n> <unit>'.",
795 s
796 )
797 })?.0;
798 Ok(RatioLiteral::Bare(n))
799 }
800 Some(unit) => {
802 if unit == "%" || unit == "%%" {
803 return Err(format!(
804 "Invalid ratio value: '{}'. '{}' must be glued to the number (e.g. '{}{}'), not separated by whitespace.",
805 s, unit, first, unit
806 ));
807 }
808 let n = first
809 .parse::<NumberLiteral>()
810 .map_err(|_| {
811 format!(
812 "Invalid ratio value: '{}'. '{}' is not a valid number.",
813 s, first
814 )
815 })?
816 .0;
817 Ok(RatioLiteral::Named {
818 value: n,
819 unit: unit.to_string(),
820 })
821 }
822 }
823 }
824}