1#[crate::polydat_node(category = Conversions)]
29fn __u64_to_string(input: u64) -> String {
30 input.to_string()
31}
32
33#[crate::polydat_node(category = Conversions)]
34fn __f64_to_string(input: f64) -> String {
35 input.to_string()
36}
37
38#[crate::polydat_node(category = Conversions)]
39fn __u64_to_f64(input: u64) -> f64 {
40 input as f64
41}
42
43#[crate::polydat_node(category = Conversions)]
44fn __bool_to_str(input: bool) -> String {
45 if input { "true".into() } else { "false".into() }
46}
47
48#[crate::polydat_node(category = Conversions)]
49fn __bool_to_u64(input: bool) -> u64 {
50 if input { 1 } else { 0 }
51}
52
53#[crate::polydat_node(category = Conversions)]
54fn __u64_to_bool(input: u64) -> bool {
55 input != 0
56}
57
58#[crate::polydat_node(category = Conversions)]
59fn __u32_to_u64(input: u32) -> u64 {
60 input as u64
61}
62
63#[crate::polydat_node(category = Conversions)]
66fn __u32_to_i64(input: u32) -> i64 {
67 input as i64
68}
69
70#[crate::polydat_node(category = Conversions)]
71fn __i32_to_i64(input: i32) -> i64 {
72 input as i64
73}
74
75#[crate::polydat_node(category = Conversions)]
76fn __f32_to_f64(input: f32) -> f64 {
77 input as f64
78}
79
80#[crate::polydat_node(category = Conversions)]
81fn __i32_to_f64(input: i32) -> f64 {
82 input as f64
83}
84
85#[crate::polydat_node(category = Conversions)]
86fn __u32_to_f64(input: u32) -> f64 {
87 input as f64
88}
89
90#[crate::polydat_node(category = Conversions)]
91fn __i64_to_f64(input: i64) -> f64 {
92 input as f64
93}
94
95#[crate::polydat_node(category = Conversions)]
96fn __i32_to_string(input: i32) -> String {
97 input.to_string()
98}
99
100#[crate::polydat_node(category = Conversions)]
101fn __i64_to_string(input: i64) -> String {
102 input.to_string()
103}
104
105#[crate::polydat_node(category = Conversions)]
106fn __f32_to_string(input: f32) -> String {
107 input.to_string()
108}
109
110#[crate::polydat_node(category = Conversions)]
111fn __u32_to_string(input: u32) -> String {
112 input.to_string()
113}
114
115#[crate::polydat_node(category = Conversions)]
130fn trunc_u64(input: f64) -> u64 {
131 if input.is_nan() {
132 0
133 } else {
134 input.trunc().max(0.0).min(u64::MAX as f64) as u64
135 }
136}
137
138#[crate::polydat_node(category = Conversions)]
141fn round_u64(input: f64) -> u64 {
142 if input.is_nan() {
143 0
144 } else {
145 input.round().max(0.0).min(u64::MAX as f64) as u64
146 }
147}
148
149fn coercion_diagnostic(raw: &str, target: &str, detail: &str) -> String {
180 let braced = format!("{{{raw}}}"); format!(
182 "value {raw:?} is not {target}.\n\n\
183 This usually means a name was written where its VALUE was intended. In a \
184 scenario `set:` block, values are text-templates, so an iteration variable \
185 must be BRACED to substitute its value:\n \
186 set: {{ field: \"{braced}\" }} # the value of `{raw}`\n \
187 set: {{ field: {raw} }} # the literal text \"{raw}\" <-- likely the bug\n\
188 In a `bindings:` block, reference names unquoted instead: `const field := {raw}`.\n\n\
189 (coercion detail: {detail})"
190 )
191}
192
193pub(crate) fn parse_bool(input: &str) -> bool {
205 let raw = input.trim();
206 match raw.to_ascii_lowercase().as_str() {
207 "true" | "1" => true,
208 "false" | "0" => false,
209 _ => panic!(
210 "{}",
211 coercion_diagnostic(
212 raw,
213 "a boolean",
214 "__str_to_bool expected case-insensitive true/false or 1/0",
215 )
216 ),
217 }
218}
219
220#[crate::polydat_node(category = Conversions)]
221fn __str_to_bool(input: &str) -> bool {
222 parse_bool(input)
223}
224
225pub(crate) fn parse_u64(input: &str) -> u64 {
228 let raw = input.trim();
229 raw.parse::<u64>().unwrap_or_else(|e| {
230 panic!(
231 "{}",
232 coercion_diagnostic(raw, "a whole number", &format!("__str_to_u64: {e}"))
233 )
234 })
235}
236
237#[crate::polydat_node(category = Conversions)]
238fn __str_to_u64(input: &str) -> u64 {
239 parse_u64(input)
240}
241
242pub(crate) fn parse_f64(input: &str) -> f64 {
245 let raw = input.trim();
246 raw.parse::<f64>().unwrap_or_else(|e| {
247 panic!(
248 "{}",
249 coercion_diagnostic(raw, "a number", &format!("__str_to_f64: {e}"))
250 )
251 })
252}
253
254#[crate::polydat_node(category = Conversions)]
255fn __str_to_f64(input: &str) -> f64 {
256 parse_f64(input)
257}
258
259#[crate::polydat_node(category = Conversions)]
275fn f64_to_u64(input: f64) -> u64 {
276 input as u64
277}
278
279#[crate::polydat_node(category = Conversions)]
280fn round_to_u64(input: f64) -> u64 {
281 input.round() as u64
282}
283
284#[crate::polydat_node(category = Conversions)]
285fn floor_to_u64(input: f64) -> u64 {
286 input.floor() as u64
287}
288
289#[crate::polydat_node(category = Conversions)]
299fn ceil_to_u64(input: f64) -> u64 {
300 input.ceil() as u64
301}
302
303#[crate::polydat_node(category = Conversions)]
327fn discretize(
328 input: f64,
329 #[poly_default(100.0f64)]
330 #[constraint(PositiveFiniteF64)]
331 range: crate::derive_support::Const<f64>,
332 #[poly_default(10u64)]
333 #[constraint(NonZeroU64)]
334 buckets: crate::derive_support::Const<u64>,
335) -> u64 {
336 let r = *range;
337 let b = *buckets;
338 let v = input.clamp(0.0, r);
339 let bucket = (v / r * b as f64) as u64;
340 bucket.min(b - 1)
341}
342
343#[crate::polydat_node(category = Conversions)]
355fn format_u64(
356 input: u64,
357 #[poly_default(10u64)] radix: crate::derive_support::Const<u64>,
358) -> String {
359 match *radix {
360 2 => format!("0b{input:b}"),
361 8 => format!("0o{input:o}"),
362 16 => format!("0x{input:x}"),
363 _ => input.to_string(),
364 }
365}
366
367impl FormatU64 {
368 pub fn decimal() -> Self {
370 Self::new(10)
371 }
372 pub fn hex() -> Self {
374 Self::new(16)
375 }
376 pub fn octal() -> Self {
378 Self::new(8)
379 }
380 pub fn binary() -> Self {
382 Self::new(2)
383 }
384 pub fn with_radix(radix: u32) -> Self {
386 Self::new(radix as u64)
387 }
388}
389
390#[crate::polydat_node(category = Conversions)]
404fn format_f64(
405 input: f64,
406 #[poly_default(2)] precision: crate::derive_support::Const<u64>,
407) -> String {
408 format!("{:.prec$}", input, prec = *precision as usize)
409}
410
411#[crate::polydat_node(category = Conversions)]
415fn zero_pad_u64(
416 input: u64,
417 #[poly_default(10)] width: crate::derive_support::Const<u64>,
418) -> String {
419 format!("{:0>width$}", input, width = *width as usize)
420}
421
422#[crate::polydat_node(category = Conversions)]
424fn to_f64(input: u64) -> f64 {
425 input as f64
426}
427
428#[crate::polydat_node(category = Conversions)]
441fn to_i64(input: u64) -> i64 {
442 if input > i64::MAX as u64 {
443 panic!("to_i64: value {input} exceeds i64::MAX ({})", i64::MAX);
444 }
445 input as i64
446}
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::ast::{PolydatNode, Value};
451
452 #[test]
453 fn f64_to_u64_truncates() {
454 let node = F64ToU64::new();
455 let mut out = [Value::None];
456 node.eval(&[Value::F64(3.7)], &mut out);
457 assert_eq!(out[0].as_u64(), 3);
458 node.eval(&[Value::F64(3.2)], &mut out);
459 assert_eq!(out[0].as_u64(), 3);
460 }
461
462 #[test]
463 fn round_to_u64_rounds() {
464 let node = RoundToU64::new();
465 let mut out = [Value::None];
466 node.eval(&[Value::F64(3.7)], &mut out);
467 assert_eq!(out[0].as_u64(), 4);
468 node.eval(&[Value::F64(3.2)], &mut out);
469 assert_eq!(out[0].as_u64(), 3);
470 }
471
472 #[test]
473 fn floor_to_u64_floors() {
474 let node = FloorToU64::new();
475 let mut out = [Value::None];
476 node.eval(&[Value::F64(3.9)], &mut out);
477 assert_eq!(out[0].as_u64(), 3);
478 }
479
480 #[test]
481 fn ceil_to_u64_ceils() {
482 let node = CeilToU64::new();
483 let mut out = [Value::None];
484 node.eval(&[Value::F64(3.1)], &mut out);
485 assert_eq!(out[0].as_u64(), 4);
486 }
487
488 #[test]
489 fn discretize_basic() {
490 let node = Discretize::new(100.0, 10);
491 let mut out = [Value::None];
492 node.eval(&[Value::F64(0.0)], &mut out);
493 assert_eq!(out[0].as_u64(), 0);
494 node.eval(&[Value::F64(55.0)], &mut out);
495 assert_eq!(out[0].as_u64(), 5);
496 node.eval(&[Value::F64(99.0)], &mut out);
497 assert_eq!(out[0].as_u64(), 9);
498 }
499
500 #[test]
501 fn discretize_clamps() {
502 let node = Discretize::new(100.0, 10);
503 let mut out = [Value::None];
504 node.eval(&[Value::F64(-5.0)], &mut out);
505 assert_eq!(out[0].as_u64(), 0);
506 node.eval(&[Value::F64(200.0)], &mut out);
507 assert_eq!(out[0].as_u64(), 9);
508 }
509
510 #[test]
511 fn format_u64_hex() {
512 let node = FormatU64::hex();
513 let mut out = [Value::None];
514 node.eval(&[Value::U64(255)], &mut out);
515 assert_eq!(out[0].as_str(), "0xff");
516 }
517
518 #[test]
519 fn format_u64_binary() {
520 let node = FormatU64::binary();
521 let mut out = [Value::None];
522 node.eval(&[Value::U64(42)], &mut out);
523 assert_eq!(out[0].as_str(), "0b101010");
524 }
525
526 #[test]
527 fn format_u64_decimal() {
528 let node = FormatU64::decimal();
529 let mut out = [Value::None];
530 node.eval(&[Value::U64(12345)], &mut out);
531 assert_eq!(out[0].as_str(), "12345");
532 }
533
534 #[test]
535 fn format_f64_precision() {
536 let node = FormatF64::new(2);
537 let mut out = [Value::None];
538 node.eval(&[Value::F64(3.14159)], &mut out);
539 assert_eq!(out[0].as_str(), "3.14");
540 }
541
542 #[test]
543 fn format_f64_zero_precision() {
544 let node = FormatF64::new(0);
545 let mut out = [Value::None];
546 node.eval(&[Value::F64(3.7)], &mut out);
547 assert_eq!(out[0].as_str(), "4");
548 }
549
550 #[test]
551 fn zero_pad() {
552 let node = ZeroPadU64::new(8);
553 let mut out = [Value::None];
554 node.eval(&[Value::U64(42)], &mut out);
555 assert_eq!(out[0].as_str(), "00000042");
556 }
557
558 #[test]
559 fn zero_pad_no_truncation() {
560 let node = ZeroPadU64::new(3);
561 let mut out = [Value::None];
562 node.eval(&[Value::U64(12345)], &mut out);
563 assert_eq!(out[0].as_str(), "12345");
564 }
565
566 #[test]
569 fn u32_to_u64_zero_extends() {
570 let node = U32ToU64::new();
571 let mut out = [Value::None];
572 node.eval(&[Value::U64(42)], &mut out);
573 assert_eq!(out[0].as_u64(), 42);
574 node.eval(&[Value::U64(0xFFFF_FFFF_0000_0001)], &mut out);
576 assert_eq!(out[0].as_u64(), 1);
577 }
578
579 #[test]
580 fn i32_to_i64_sign_extends() {
581 let node = I32ToI64::new();
582 let mut out = [Value::None];
583 node.eval(&[Value::U64(42)], &mut out);
587 assert_eq!(out[0], Value::I64(42));
588 node.eval(&[Value::U64(0xFFFF_FFFF)], &mut out);
591 assert_eq!(out[0], Value::I64(-1));
592 node.eval(&[Value::I64(-1)], &mut out);
594 assert_eq!(out[0], Value::I64(-1));
595 }
596
597 #[test]
598 fn f32_to_f64_widens() {
599 let node = F32ToF64::new();
600 let mut out = [Value::None];
601 let f32_bits = 3.14f32.to_bits() as u64;
602 node.eval(&[Value::U64(f32_bits)], &mut out);
603 let result = out[0].as_f64();
605 assert!((result - 3.14).abs() < 0.001, "got {result}");
606 }
607
608 #[test]
609 fn i32_to_f64_converts() {
610 let node = I32ToF64::new();
611 let mut out = [Value::None];
612 node.eval(&[Value::U64(42)], &mut out);
613 assert_eq!(out[0].as_f64(), 42.0);
614 node.eval(&[Value::U64((-10i32) as u32 as u64)], &mut out);
616 assert_eq!(out[0].as_f64(), -10.0);
617 }
618
619 #[test]
620 fn u32_to_f64_converts() {
621 let node = U32ToF64::new();
622 let mut out = [Value::None];
623 node.eval(&[Value::U64(1000)], &mut out);
624 assert_eq!(out[0].as_f64(), 1000.0);
625 }
626
627 #[test]
628 fn i64_to_f64_converts() {
629 let node = I64ToF64::new();
630 let mut out = [Value::None];
631 node.eval(&[Value::U64(42)], &mut out);
632 assert_eq!(out[0].as_f64(), 42.0);
633 node.eval(&[Value::U64((-1i64) as u64)], &mut out);
635 assert_eq!(out[0].as_f64(), -1.0);
636 }
637
638 #[test]
641 fn i32_to_string_formats_signed() {
642 let node = I32ToString::new();
643 let mut out = [Value::None];
644 node.eval(&[Value::U64(42)], &mut out);
645 assert_eq!(out[0].as_str(), "42");
646 node.eval(&[Value::U64((-7i32) as u32 as u64)], &mut out);
647 assert_eq!(out[0].as_str(), "-7");
648 }
649
650 #[test]
651 fn i64_to_string_formats_signed() {
652 let node = I64ToString::new();
653 let mut out = [Value::None];
654 node.eval(&[Value::U64(100)], &mut out);
655 assert_eq!(out[0].as_str(), "100");
656 node.eval(&[Value::U64((-42i64) as u64)], &mut out);
657 assert_eq!(out[0].as_str(), "-42");
658 }
659
660 #[test]
661 fn f32_to_string_formats() {
662 let node = F32ToString::new();
663 let mut out = [Value::None];
664 let bits = 2.5f32.to_bits() as u64;
665 node.eval(&[Value::U64(bits)], &mut out);
666 assert_eq!(out[0].as_str(), "2.5");
667 }
668
669 #[test]
670 fn u32_to_string_formats() {
671 let node = U32ToString::new();
672 let mut out = [Value::None];
673 node.eval(&[Value::U64(12345)], &mut out);
674 assert_eq!(out[0].as_str(), "12345");
675 }
676
677 #[test]
682 fn str_to_bool_canonical_forms() {
683 let node = StrToBool::new();
684 let mut out = [Value::None];
685 for (input, expected) in [
686 ("true", true),
687 ("false", false),
688 ("True", true),
689 ("False", false),
690 ("TRUE", true),
691 ("FALSE", false),
692 ("1", true),
693 ("0", false),
694 ] {
695 node.eval(&[Value::Str(input.into())], &mut out);
696 assert_eq!(out[0].as_bool(), expected, "input={input:?}");
697 }
698 }
699
700 #[test]
701 fn str_to_bool_trims_whitespace() {
702 let node = StrToBool::new();
703 let mut out = [Value::None];
704 node.eval(&[Value::Str(" true ".into())], &mut out);
705 assert!(out[0].as_bool());
706 node.eval(&[Value::Str("\tfalse\n".into())], &mut out);
707 assert!(!out[0].as_bool());
708 }
709
710 #[test]
711 #[should_panic(expected = "__str_to_bool")]
712 fn str_to_bool_panics_on_unparseable() {
713 let node = StrToBool::new();
714 let mut out = [Value::None];
715 node.eval(&[Value::Str("yes".into())], &mut out);
716 }
717
718 #[test]
719 fn str_to_u64_basic() {
720 let node = StrToU64::new();
721 let mut out = [Value::None];
722 node.eval(&[Value::Str("0".into())], &mut out);
723 assert_eq!(out[0].as_u64(), 0);
724 node.eval(&[Value::Str("42".into())], &mut out);
725 assert_eq!(out[0].as_u64(), 42);
726 node.eval(&[Value::Str("18446744073709551615".into())], &mut out);
727 assert_eq!(out[0].as_u64(), u64::MAX);
728 }
729
730 #[test]
731 fn str_to_u64_trims_whitespace() {
732 let node = StrToU64::new();
733 let mut out = [Value::None];
734 node.eval(&[Value::Str(" 42 ".into())], &mut out);
735 assert_eq!(out[0].as_u64(), 42);
736 }
737
738 #[test]
739 #[should_panic(expected = "__str_to_u64")]
740 fn str_to_u64_panics_on_negative() {
741 let node = StrToU64::new();
742 let mut out = [Value::None];
743 node.eval(&[Value::Str("-1".into())], &mut out);
744 }
745
746 #[test]
747 #[should_panic(expected = "__str_to_u64")]
748 fn str_to_u64_panics_on_garbage() {
749 let node = StrToU64::new();
750 let mut out = [Value::None];
751 node.eval(&[Value::Str("abc".into())], &mut out);
752 }
753
754 #[test]
755 fn str_to_f64_basic() {
756 let node = StrToF64::new();
757 let mut out = [Value::None];
758 node.eval(&[Value::Str("0.0".into())], &mut out);
759 assert_eq!(out[0].as_f64(), 0.0);
760 node.eval(&[Value::Str("3.14".into())], &mut out);
761 assert!((out[0].as_f64() - 3.14).abs() < 1e-12);
762 node.eval(&[Value::Str("-2.5e3".into())], &mut out);
763 assert_eq!(out[0].as_f64(), -2500.0);
764 node.eval(&[Value::Str("inf".into())], &mut out);
765 assert!(out[0].as_f64().is_infinite());
766 }
767
768 #[test]
769 fn str_to_f64_trims_whitespace() {
770 let node = StrToF64::new();
771 let mut out = [Value::None];
772 node.eval(&[Value::Str(" 1.5 ".into())], &mut out);
773 assert_eq!(out[0].as_f64(), 1.5);
774 }
775
776 #[test]
777 #[should_panic(expected = "__str_to_f64")]
778 fn str_to_f64_panics_on_garbage() {
779 let node = StrToF64::new();
780 let mut out = [Value::None];
781 node.eval(&[Value::Str("not-a-number".into())], &mut out);
782 }
783
784 #[test]
788 fn narrowing_casts_saturate() {
789 let t = TruncU64::new();
790 let r = RoundU64::new();
791 let mut out = [Value::None];
792 t.eval(&[Value::F64(900.9)], &mut out);
793 assert_eq!(out[0].as_u64(), 900, "trunc drops the fraction");
794 r.eval(&[Value::F64(900.9)], &mut out);
795 assert_eq!(out[0].as_u64(), 901, "round goes to nearest");
796 t.eval(&[Value::F64(-5.0)], &mut out);
797 assert_eq!(out[0].as_u64(), 0, "negative saturates to 0");
798 r.eval(&[Value::F64(f64::NAN)], &mut out);
799 assert_eq!(out[0].as_u64(), 0, "NaN saturates to 0");
800 t.eval(&[Value::F64(f64::INFINITY)], &mut out);
801 assert_eq!(out[0].as_u64(), u64::MAX, "overflow saturates to MAX");
802 }
803}