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