1use crate::diagnostic::codes;
24use std::collections::HashSet;
25use std::sync::OnceLock;
26
27use thiserror::Error;
28
29#[derive(Error, Debug, Clone, PartialEq, Eq)]
35#[error("ClosedFunctionError({code}): {message}")]
36pub struct ClosedFunctionError {
37 pub code: String,
42
43 pub message: String,
45}
46
47impl ClosedFunctionError {
48 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
49 Self {
50 code: code.into(),
51 message: message.into(),
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
61pub enum ClosedArg {
62 Scalar(f64),
64
65 Array(Vec<f64>),
68
69 Array2D(Vec<Vec<f64>>),
73}
74
75impl ClosedArg {
76 fn expect_scalar(&self, name: &str, position: usize) -> Result<f64, ClosedFunctionError> {
81 match self {
82 ClosedArg::Scalar(v) => Ok(*v),
83 ClosedArg::Array(_) | ClosedArg::Array2D(_) => Err(ClosedFunctionError::new(
84 codes::CLOSED_FUNCTION_ARG_TYPE,
85 format!("{name}: arg #{} must be scalar, got array", position + 1),
86 )),
87 }
88 }
89
90 fn expect_array(&self, name: &str, position: usize) -> Result<&[f64], ClosedFunctionError> {
91 match self {
92 ClosedArg::Array(v) => Ok(v.as_slice()),
93 ClosedArg::Scalar(_) | ClosedArg::Array2D(_) => Err(ClosedFunctionError::new(
94 codes::CLOSED_FUNCTION_ARG_TYPE,
95 format!(
96 "{name}: arg #{} must be 1-D array, got other shape",
97 position + 1
98 ),
99 )),
100 }
101 }
102
103 fn expect_array2d(
104 &self,
105 name: &str,
106 position: usize,
107 ) -> Result<&[Vec<f64>], ClosedFunctionError> {
108 match self {
109 ClosedArg::Array2D(v) => Ok(v.as_slice()),
110 ClosedArg::Scalar(_) | ClosedArg::Array(_) => Err(ClosedFunctionError::new(
111 codes::CLOSED_FUNCTION_ARG_TYPE,
112 format!(
113 "{name}: arg #{} must be 2-D array, got other shape",
114 position + 1
115 ),
116 )),
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq)]
126pub enum ClosedValue {
127 Integer(i32),
131
132 Float(f64),
134}
135
136impl ClosedValue {
137 pub fn as_f64(self) -> f64 {
142 match self {
143 ClosedValue::Integer(i) => i as f64,
144 ClosedValue::Float(f) => f,
145 }
146 }
147}
148
149type ClosedHandler = fn(&str, &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError>;
152
153const CLOSED_FUNCTIONS: &[(&str, ClosedHandler)] = &[
160 ("datetime.year", cf_datetime_year),
161 ("datetime.month", |name, args| {
162 datetime_field(name, args, |(_, m, _, _, _, _)| m)
163 }),
164 ("datetime.day", |name, args| {
165 datetime_field(name, args, |(_, _, d, _, _, _)| d)
166 }),
167 ("datetime.hour", |name, args| {
168 datetime_field(name, args, |(_, _, _, h, _, _)| h)
169 }),
170 ("datetime.minute", |name, args| {
171 datetime_field(name, args, |(_, _, _, _, mi, _)| mi)
172 }),
173 ("datetime.second", |name, args| {
174 datetime_field(name, args, |(_, _, _, _, _, s)| s)
175 }),
176 ("datetime.day_of_year", cf_datetime_day_of_year),
177 ("datetime.julian_day", cf_datetime_julian_day),
178 ("datetime.is_leap_year", cf_datetime_is_leap_year),
179 ("interp.searchsorted", cf_interp_searchsorted),
180 ("interp.linear", cf_interp_linear),
181 ("interp.bilinear", cf_interp_bilinear),
182];
183
184pub fn closed_function_names() -> &'static HashSet<String> {
187 static NAMES: OnceLock<HashSet<String>> = OnceLock::new();
188 NAMES.get_or_init(|| {
189 CLOSED_FUNCTIONS
190 .iter()
191 .map(|(n, _)| (*n).to_string())
192 .collect()
193 })
194}
195
196pub fn evaluate_closed_function(
201 name: &str,
202 args: &[ClosedArg],
203) -> Result<ClosedValue, ClosedFunctionError> {
204 match CLOSED_FUNCTIONS.iter().find(|(n, _)| *n == name) {
205 Some((n, handler)) => handler(n, args),
206 None => Err(ClosedFunctionError::new(
207 codes::UNKNOWN_CLOSED_FUNCTION,
208 format!(
209 "`fn` name `{name}` is not in the v0.3.0 closed function registry \
210 (esm-spec §9.2). Adding a primitive requires a spec rev."
211 ),
212 )),
213 }
214}
215
216fn cf_datetime_year(name: &str, args: &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError> {
217 expect_arity(name, args, 1)?;
218 let t = args[0].expect_scalar(name, 0)?;
219 let (y, _, _, _, _, _) = decompose_utc(t);
220 Ok(ClosedValue::Integer(check_i32(name, y as i64)?))
221}
222
223fn cf_datetime_day_of_year(
224 name: &str,
225 args: &[ClosedArg],
226) -> Result<ClosedValue, ClosedFunctionError> {
227 expect_arity(name, args, 1)?;
228 let t = args[0].expect_scalar(name, 0)?;
229 let (y, m, d, _, _, _) = decompose_utc(t);
230 Ok(ClosedValue::Integer(day_of_year(y, m, d) as i32))
231}
232
233fn cf_datetime_julian_day(
234 name: &str,
235 args: &[ClosedArg],
236) -> Result<ClosedValue, ClosedFunctionError> {
237 expect_arity(name, args, 1)?;
238 let t = args[0].expect_scalar(name, 0)?;
239 Ok(ClosedValue::Float(julian_day(t)))
240}
241
242fn cf_datetime_is_leap_year(
243 name: &str,
244 args: &[ClosedArg],
245) -> Result<ClosedValue, ClosedFunctionError> {
246 expect_arity(name, args, 1)?;
247 let t = args[0].expect_scalar(name, 0)?;
248 let (y, _, _, _, _, _) = decompose_utc(t);
249 Ok(ClosedValue::Integer(if is_leap_year(y) { 1 } else { 0 }))
250}
251
252fn cf_interp_searchsorted(
253 name: &str,
254 args: &[ClosedArg],
255) -> Result<ClosedValue, ClosedFunctionError> {
256 expect_arity(name, args, 2)?;
257 let x = args[0].expect_scalar(name, 0)?;
258 let xs = args[1].expect_array(name, 1)?;
259 Ok(ClosedValue::Integer(check_i32(
260 name,
261 searchsorted(name, x, xs)?,
262 )?))
263}
264
265fn cf_interp_linear(name: &str, args: &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError> {
266 expect_arity(name, args, 3)?;
267 let table = args[0].expect_array(name, 0)?;
268 let axis = args[1].expect_array(name, 1)?;
269 let x = args[2].expect_scalar(name, 2)?;
270 Ok(ClosedValue::Float(interp_linear(table, axis, x)?))
271}
272
273fn cf_interp_bilinear(name: &str, args: &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError> {
274 expect_arity(name, args, 5)?;
275 let table = args[0].expect_array2d(name, 0)?;
276 let axis_x = args[1].expect_array(name, 1)?;
277 let axis_y = args[2].expect_array(name, 2)?;
278 let x = args[3].expect_scalar(name, 3)?;
279 let y = args[4].expect_scalar(name, 4)?;
280 Ok(ClosedValue::Float(interp_bilinear(
281 table, axis_x, axis_y, x, y,
282 )?))
283}
284
285fn datetime_field(
288 name: &str,
289 args: &[ClosedArg],
290 select: impl Fn((i32, u32, u32, u32, u32, u32)) -> u32,
291) -> Result<ClosedValue, ClosedFunctionError> {
292 expect_arity(name, args, 1)?;
293 let t = args[0].expect_scalar(name, 0)?;
294 Ok(ClosedValue::Integer(select(decompose_utc(t)) as i32))
295}
296
297fn expect_arity(name: &str, args: &[ClosedArg], n: usize) -> Result<(), ClosedFunctionError> {
298 if args.len() != n {
299 return Err(ClosedFunctionError::new(
300 codes::CLOSED_FUNCTION_ARITY,
301 format!("{name} expects {n} argument(s), got {}", args.len()),
302 ));
303 }
304 Ok(())
305}
306
307fn check_i32(name: &str, v: i64) -> Result<i32, ClosedFunctionError> {
308 if v < i32::MIN as i64 || v > i32::MAX as i64 {
309 return Err(ClosedFunctionError::new(
310 codes::CLOSED_FUNCTION_OVERFLOW,
311 format!("{name}: result {v} overflows Int32"),
312 ));
313 }
314 Ok(v as i32)
315}
316
317#[inline]
328fn floor_div_mod(t_utc: f64) -> (i64, f64) {
329 let day = (t_utc / 86400.0).floor();
330 let mut sec = t_utc - day * 86400.0;
331 if sec >= 86400.0 {
334 sec -= 86400.0;
335 } else if sec < 0.0 {
336 sec += 86400.0;
337 }
338 (day as i64, sec)
339}
340
341fn decompose_utc(t_utc: f64) -> (i32, u32, u32, u32, u32, u32) {
346 let (day_count, sec_in_day) = floor_div_mod(t_utc);
347 let (y, m, d) = civil_from_days(day_count);
348 let s = sec_in_day.trunc() as i64;
351 let hour = (s / 3600) as u32;
352 let minute = ((s % 3600) / 60) as u32;
353 let second = (s % 60) as u32;
354 (y, m, d, hour, minute, second)
355}
356
357fn civil_from_days(z: i64) -> (i32, u32, u32) {
361 let z = z + 719468;
362 let era = if z >= 0 { z } else { z - 146096 } / 146097;
363 let doe = (z - era * 146097) as u64; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = (yoe as i64) + era * 400;
366 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; let y = if m <= 2 { y + 1 } else { y };
371 (y as i32, m, d)
372}
373
374fn is_leap_year(y: i32) -> bool {
375 (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
376}
377
378fn day_of_year(y: i32, m: u32, d: u32) -> u32 {
379 const CUM: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
380 let mut doy = CUM[(m - 1) as usize] + d;
381 if m > 2 && is_leap_year(y) {
382 doy += 1;
383 }
384 doy
385}
386
387fn julian_day(t_utc: f64) -> f64 {
395 let (day_count, sec_in_day) = floor_div_mod(t_utc);
396 let (y, m, d) = civil_from_days(day_count);
397 let y = y as i64;
398 let m = m as i64;
399 let d = d as i64;
400 let jdn = (1461 * (y + 4800 + (m - 14) / 12)) / 4 + (367 * (m - 2 - 12 * ((m - 14) / 12))) / 12
401 - (3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4
402 + d
403 - 32075;
404 (jdn as f64) + (sec_in_day - 43200.0) / 86400.0
405}
406
407fn searchsorted(name: &str, x: f64, xs: &[f64]) -> Result<i64, ClosedFunctionError> {
411 let mut prev = f64::NAN;
413 for (i, v) in xs.iter().copied().enumerate() {
414 if v.is_nan() {
415 return Err(ClosedFunctionError::new(
416 codes::SEARCHSORTED_NAN_IN_TABLE,
417 format!(
418 "{name}: xs[{idx}] is NaN; NaN entries in xs are forbidden",
419 idx = i + 1
420 ),
421 ));
422 }
423 if i > 0 && v < prev {
424 return Err(ClosedFunctionError::new(
425 codes::SEARCHSORTED_NON_MONOTONIC,
426 format!(
427 "{name}: xs is not non-decreasing (xs[{idx}]={v} < xs[{prev_idx}]={prev})",
428 idx = i + 1,
429 prev_idx = i,
430 ),
431 ));
432 }
433 prev = v;
434 }
435 Ok(searchsorted_at(x, xs))
436}
437
438pub(crate) fn searchsorted_at(x: f64, xs: &[f64]) -> i64 {
447 let n = xs.len();
448 if n == 0 {
450 return 1;
451 }
452 if x.is_nan() {
453 return (n as i64) + 1;
454 }
455 for (i, v) in xs.iter().copied().enumerate() {
456 if v >= x {
457 return (i as i64) + 1;
458 }
459 }
460 (n as i64) + 1
461}
462
463fn interp_linear(table: &[f64], axis: &[f64], x: f64) -> Result<f64, ClosedFunctionError> {
470 validate_axis("interp.linear", "axis", axis)?;
471 if table.len() != axis.len() {
472 return Err(ClosedFunctionError::new(
473 codes::INTERP_AXIS_LENGTH_MISMATCH,
474 format!(
475 "interp.linear: len(table)={} != len(axis)={}",
476 table.len(),
477 axis.len()
478 ),
479 ));
480 }
481 Ok(interp_linear_at(table, axis, x))
482}
483
484pub(crate) fn interp_linear_at(table: &[f64], axis: &[f64], x: f64) -> f64 {
493 let n = axis.len();
494 if x <= axis[0] {
495 return table[0];
496 }
497 if x >= axis[n - 1] {
498 return table[n - 1];
499 }
500 let mut i = 0usize;
504 if x.is_nan() {
505 i = 0;
508 } else {
509 for k in 0..(n - 1) {
510 if axis[k] <= x && x < axis[k + 1] {
511 i = k;
512 break;
513 }
514 }
515 }
516 let w = (x - axis[i]) / (axis[i + 1] - axis[i]);
517 table[i] + w * (table[i + 1] - table[i])
518}
519
520fn interp_bilinear(
527 table: &[Vec<f64>],
528 axis_x: &[f64],
529 axis_y: &[f64],
530 x: f64,
531 y: f64,
532) -> Result<f64, ClosedFunctionError> {
533 validate_axis("interp.bilinear", "axis_x", axis_x)?;
534 validate_axis("interp.bilinear", "axis_y", axis_y)?;
535 if table.len() != axis_x.len() {
536 return Err(ClosedFunctionError::new(
537 codes::INTERP_AXIS_LENGTH_MISMATCH,
538 format!(
539 "interp.bilinear: outer len(table)={} != len(axis_x)={}",
540 table.len(),
541 axis_x.len()
542 ),
543 ));
544 }
545 let ny = axis_y.len();
546 for (i, row) in table.iter().enumerate() {
547 if row.len() != ny {
548 return Err(ClosedFunctionError::new(
549 codes::INTERP_AXIS_LENGTH_MISMATCH,
550 format!(
551 "interp.bilinear: table row {row_idx} has len={got} but len(axis_y)={ny}",
552 row_idx = i + 1,
553 got = row.len(),
554 ),
555 ));
556 }
557 }
558 Ok(bilinear_at(|i, j| table[i][j], axis_x, axis_y, x, y))
559}
560
561pub(crate) fn interp_bilinear_at(
569 table: &[f64],
570 axis_x: &[f64],
571 axis_y: &[f64],
572 x: f64,
573 y: f64,
574) -> f64 {
575 let ny = axis_y.len();
576 bilinear_at(|i, j| table[i * ny + j], axis_x, axis_y, x, y)
577}
578
579fn bilinear_at(
583 at: impl Fn(usize, usize) -> f64,
584 axis_x: &[f64],
585 axis_y: &[f64],
586 x: f64,
587 y: f64,
588) -> f64 {
589 let nx = axis_x.len();
590 let ny = axis_y.len();
591 let x_q = if x <= axis_x[0] {
595 axis_x[0]
596 } else if x >= axis_x[nx - 1] {
597 axis_x[nx - 1]
598 } else {
599 x
600 };
601 let y_q = if y <= axis_y[0] {
602 axis_y[0]
603 } else if y >= axis_y[ny - 1] {
604 axis_y[ny - 1]
605 } else {
606 y
607 };
608 let i = locate_cell(axis_x, x_q);
614 let j = locate_cell(axis_y, y_q);
615 let wx = (x_q - axis_x[i]) / (axis_x[i + 1] - axis_x[i]);
616 let wy = (y_q - axis_y[j]) / (axis_y[j + 1] - axis_y[j]);
617 let row_j = at(i, j) + wx * (at(i + 1, j) - at(i, j));
618 let row_jp1 = at(i, j + 1) + wx * (at(i + 1, j + 1) - at(i, j + 1));
619 row_j + wy * (row_jp1 - row_j)
620}
621
622fn locate_cell(axis: &[f64], q: f64) -> usize {
627 let last = axis.len() - 2;
628 let mut i = 0usize;
629 for (k, v) in axis.iter().take(last + 1).enumerate() {
630 if *v <= q {
631 i = k;
632 } else {
633 break;
634 }
635 }
636 i
637}
638
639fn validate_axis(fn_name: &str, label: &str, axis: &[f64]) -> Result<(), ClosedFunctionError> {
646 if axis.len() < 2 {
647 return Err(ClosedFunctionError::new(
648 codes::INTERP_AXIS_TOO_SHORT,
649 format!(
650 "{fn_name}: {label} has {len} entries; need at least 2 (no interval to blend across)",
651 len = axis.len()
652 ),
653 ));
654 }
655 for (i, v) in axis.iter().copied().enumerate() {
656 if v.is_nan() {
657 return Err(ClosedFunctionError::new(
658 codes::INTERP_NAN_IN_AXIS,
659 format!(
660 "{fn_name}: {label}[{idx}] is NaN; NaN entries in axes are forbidden",
661 idx = i + 1
662 ),
663 ));
664 }
665 }
666 for w in axis.windows(2) {
671 if w[0] >= w[1] {
672 return Err(ClosedFunctionError::new(
673 codes::INTERP_NON_MONOTONIC_AXIS,
674 format!(
675 "{fn_name}: {label} is not strictly increasing (encountered {a} then {b})",
676 a = w[0],
677 b = w[1]
678 ),
679 ));
680 }
681 }
682 Ok(())
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 fn s(v: f64) -> ClosedArg {
690 ClosedArg::Scalar(v)
691 }
692 fn a(vs: &[f64]) -> ClosedArg {
693 ClosedArg::Array(vs.to_vec())
694 }
695 fn a2(rows: &[&[f64]]) -> ClosedArg {
696 ClosedArg::Array2D(rows.iter().map(|r| r.to_vec()).collect())
697 }
698
699 #[test]
700 fn unknown_name_diagnostic() {
701 let err = evaluate_closed_function("datetime.century", &[s(0.0)]).unwrap_err();
702 assert_eq!(err.code, "unknown_closed_function");
703 }
704
705 #[test]
706 fn closed_function_names_v030() {
707 let names = closed_function_names();
708 assert_eq!(names.len(), 12);
709 for n in [
710 "datetime.year",
711 "datetime.month",
712 "datetime.day",
713 "datetime.hour",
714 "datetime.minute",
715 "datetime.second",
716 "datetime.day_of_year",
717 "datetime.julian_day",
718 "datetime.is_leap_year",
719 "interp.searchsorted",
720 "interp.linear",
721 "interp.bilinear",
722 ] {
723 assert!(names.contains(n), "missing {n}");
724 }
725 }
726
727 #[test]
728 fn epoch_year() {
729 let v = evaluate_closed_function("datetime.year", &[s(0.0)]).unwrap();
730 assert_eq!(v, ClosedValue::Integer(1970));
731 }
732
733 #[test]
734 fn pre_epoch_year() {
735 let v = evaluate_closed_function("datetime.year", &[s(-1.0)]).unwrap();
737 assert_eq!(v, ClosedValue::Integer(1969));
738 }
739
740 #[test]
741 fn leap_year_2000() {
742 let v = evaluate_closed_function("datetime.is_leap_year", &[s(951825600.0)]).unwrap();
744 assert_eq!(v, ClosedValue::Integer(1));
745 }
746
747 #[test]
748 fn not_leap_2100() {
749 let v = evaluate_closed_function("datetime.is_leap_year", &[s(4107542399.0)]).unwrap();
751 assert_eq!(v, ClosedValue::Integer(0));
752 }
753
754 #[test]
755 fn day_of_year_leap_dec31() {
756 let v = evaluate_closed_function("datetime.day_of_year", &[s(1735689599.0)]).unwrap();
758 assert_eq!(v, ClosedValue::Integer(366));
759 }
760
761 #[test]
762 fn searchsorted_exact_boundary() {
763 let v = evaluate_closed_function(
764 "interp.searchsorted",
765 &[s(2.0), a(&[1.0, 2.0, 3.0, 4.0, 5.0])],
766 )
767 .unwrap();
768 assert_eq!(v, ClosedValue::Integer(2));
769 }
770
771 #[test]
772 fn searchsorted_above_range() {
773 let v = evaluate_closed_function(
774 "interp.searchsorted",
775 &[s(10.0), a(&[1.0, 2.0, 3.0, 4.0, 5.0])],
776 )
777 .unwrap();
778 assert_eq!(v, ClosedValue::Integer(6));
779 }
780
781 #[test]
782 fn searchsorted_nan_x() {
783 let v = evaluate_closed_function(
784 "interp.searchsorted",
785 &[s(f64::NAN), a(&[1.0, 2.0, 3.0, 4.0, 5.0])],
786 )
787 .unwrap();
788 assert_eq!(v, ClosedValue::Integer(6));
789 }
790
791 #[test]
792 fn searchsorted_non_monotonic_rejects() {
793 let err = evaluate_closed_function(
794 "interp.searchsorted",
795 &[s(2.0), a(&[1.0, 3.0, 2.0, 4.0, 5.0])],
796 )
797 .unwrap_err();
798 assert_eq!(err.code, "searchsorted_non_monotonic");
799 }
800
801 #[test]
802 fn searchsorted_nan_in_xs_rejects() {
803 let err =
804 evaluate_closed_function("interp.searchsorted", &[s(2.0), a(&[1.0, f64::NAN, 3.0])])
805 .unwrap_err();
806 assert_eq!(err.code, "searchsorted_nan_in_table");
807 }
808
809 #[test]
810 fn searchsorted_duplicates_left_bias() {
811 let v = evaluate_closed_function(
812 "interp.searchsorted",
813 &[s(2.0), a(&[1.0, 2.0, 2.0, 2.0, 3.0])],
814 )
815 .unwrap();
816 assert_eq!(v, ClosedValue::Integer(2));
817 }
818
819 #[test]
820 fn interp_linear_exact_at_knot() {
821 let v = evaluate_closed_function(
822 "interp.linear",
823 &[
824 a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
825 a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
826 s(2.0),
827 ],
828 )
829 .unwrap();
830 assert_eq!(v.as_f64(), 40.0);
831 }
832
833 #[test]
834 fn interp_linear_midpoint_blend() {
835 let v = evaluate_closed_function(
836 "interp.linear",
837 &[
838 a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
839 a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
840 s(0.5),
841 ],
842 )
843 .unwrap();
844 assert_eq!(v.as_f64(), 15.0);
845 }
846
847 #[test]
848 fn interp_linear_below_clamps() {
849 let v = evaluate_closed_function(
850 "interp.linear",
851 &[a(&[3.0, 7.0]), a(&[10.0, 20.0]), s(-100.0)],
852 )
853 .unwrap();
854 assert_eq!(v.as_f64(), 3.0);
855 }
856
857 #[test]
858 fn interp_linear_above_clamps_exact_endpoint() {
859 let v = evaluate_closed_function(
862 "interp.linear",
863 &[
864 a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
865 a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
866 s(99.0),
867 ],
868 )
869 .unwrap();
870 assert_eq!(v.as_f64(), 160.0);
871 }
872
873 #[test]
874 fn interp_linear_nan_propagates() {
875 let v = evaluate_closed_function(
876 "interp.linear",
877 &[
878 a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
879 a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
880 s(f64::NAN),
881 ],
882 )
883 .unwrap();
884 assert!(v.as_f64().is_nan());
885 }
886
887 #[test]
888 fn interp_linear_non_monotonic_rejects() {
889 let err = evaluate_closed_function(
890 "interp.linear",
891 &[
892 a(&[10.0, 20.0, 30.0, 40.0]),
893 a(&[0.0, 2.0, 1.0, 3.0]),
894 s(1.5),
895 ],
896 )
897 .unwrap_err();
898 assert_eq!(err.code, "interp_non_monotonic_axis");
899 }
900
901 #[test]
902 fn interp_linear_equal_adjacent_rejects() {
903 let err = evaluate_closed_function(
904 "interp.linear",
905 &[
906 a(&[10.0, 20.0, 30.0, 40.0]),
907 a(&[0.0, 1.0, 1.0, 2.0]),
908 s(0.5),
909 ],
910 )
911 .unwrap_err();
912 assert_eq!(err.code, "interp_non_monotonic_axis");
913 }
914
915 #[test]
916 fn interp_linear_length_mismatch_rejects() {
917 let err = evaluate_closed_function(
918 "interp.linear",
919 &[a(&[10.0, 20.0, 30.0, 40.0]), a(&[0.0, 1.0, 2.0]), s(1.0)],
920 )
921 .unwrap_err();
922 assert_eq!(err.code, "interp_axis_length_mismatch");
923 }
924
925 #[test]
926 fn interp_linear_nan_in_axis_rejects() {
927 let err = evaluate_closed_function(
928 "interp.linear",
929 &[
930 a(&[10.0, 20.0, 30.0, 40.0]),
931 a(&[0.0, f64::NAN, 2.0, 3.0]),
932 s(1.0),
933 ],
934 )
935 .unwrap_err();
936 assert_eq!(err.code, "interp_nan_in_axis");
937 }
938
939 #[test]
940 fn interp_linear_axis_too_short_rejects() {
941 let err = evaluate_closed_function("interp.linear", &[a(&[42.0]), a(&[0.0]), s(0.0)])
942 .unwrap_err();
943 assert_eq!(err.code, "interp_axis_too_short");
944 }
945
946 #[test]
947 fn interp_bilinear_exact_corner() {
948 let v = evaluate_closed_function(
949 "interp.bilinear",
950 &[
951 a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
952 a(&[0.0, 1.0, 2.0]),
953 a(&[0.0, 10.0, 20.0]),
954 s(2.0),
955 s(20.0),
956 ],
957 )
958 .unwrap();
959 assert_eq!(v.as_f64(), 22.0);
960 }
961
962 #[test]
963 fn interp_bilinear_center_blend() {
964 let v = evaluate_closed_function(
965 "interp.bilinear",
966 &[
967 a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
968 a(&[0.0, 1.0, 2.0]),
969 a(&[0.0, 10.0, 20.0]),
970 s(0.5),
971 s(5.0),
972 ],
973 )
974 .unwrap();
975 assert_eq!(v.as_f64(), 5.5);
976 }
977
978 #[test]
979 fn interp_bilinear_clamps_to_corner() {
980 let v = evaluate_closed_function(
981 "interp.bilinear",
982 &[
983 a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
984 a(&[0.0, 1.0, 2.0]),
985 a(&[0.0, 10.0, 20.0]),
986 s(1000.0),
987 s(1000.0),
988 ],
989 )
990 .unwrap();
991 assert_eq!(v.as_f64(), 22.0);
992 }
993
994 #[test]
995 fn interp_bilinear_nan_x_propagates() {
996 let v = evaluate_closed_function(
997 "interp.bilinear",
998 &[
999 a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
1000 a(&[0.0, 1.0, 2.0]),
1001 a(&[0.0, 10.0, 20.0]),
1002 s(f64::NAN),
1003 s(5.0),
1004 ],
1005 )
1006 .unwrap();
1007 assert!(v.as_f64().is_nan());
1008 }
1009
1010 #[test]
1011 fn interp_bilinear_ragged_rows_reject() {
1012 let err = evaluate_closed_function(
1013 "interp.bilinear",
1014 &[
1015 a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0], &[20.0, 21.0, 22.0]]),
1016 a(&[0.0, 1.0, 2.0]),
1017 a(&[0.0, 10.0, 20.0]),
1018 s(0.5),
1019 s(5.0),
1020 ],
1021 )
1022 .unwrap_err();
1023 assert_eq!(err.code, "interp_axis_length_mismatch");
1024 }
1025
1026 #[test]
1027 fn interp_bilinear_axis_x_length_mismatch_rejects() {
1028 let err = evaluate_closed_function(
1029 "interp.bilinear",
1030 &[
1031 a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
1032 a(&[0.0, 1.0]),
1033 a(&[0.0, 10.0, 20.0]),
1034 s(0.5),
1035 s(5.0),
1036 ],
1037 )
1038 .unwrap_err();
1039 assert_eq!(err.code, "interp_axis_length_mismatch");
1040 }
1041
1042 #[test]
1043 fn interp_bilinear_non_monotonic_axis_rejects() {
1044 let err = evaluate_closed_function(
1045 "interp.bilinear",
1046 &[
1047 a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
1048 a(&[0.0, 2.0, 1.0]),
1049 a(&[0.0, 10.0, 20.0]),
1050 s(0.5),
1051 s(5.0),
1052 ],
1053 )
1054 .unwrap_err();
1055 assert_eq!(err.code, "interp_non_monotonic_axis");
1056 }
1057}