use crate::diagnostic::codes;
use std::collections::HashSet;
use std::sync::OnceLock;
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[error("ClosedFunctionError({code}): {message}")]
pub struct ClosedFunctionError {
pub code: String,
pub message: String,
}
impl ClosedFunctionError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
}
}
}
#[derive(Debug, Clone)]
pub enum ClosedArg {
Scalar(f64),
Array(Vec<f64>),
Array2D(Vec<Vec<f64>>),
}
impl ClosedArg {
fn expect_scalar(&self, name: &str, position: usize) -> Result<f64, ClosedFunctionError> {
match self {
ClosedArg::Scalar(v) => Ok(*v),
ClosedArg::Array(_) | ClosedArg::Array2D(_) => Err(ClosedFunctionError::new(
codes::CLOSED_FUNCTION_ARG_TYPE,
format!("{name}: arg #{} must be scalar, got array", position + 1),
)),
}
}
fn expect_array(&self, name: &str, position: usize) -> Result<&[f64], ClosedFunctionError> {
match self {
ClosedArg::Array(v) => Ok(v.as_slice()),
ClosedArg::Scalar(_) | ClosedArg::Array2D(_) => Err(ClosedFunctionError::new(
codes::CLOSED_FUNCTION_ARG_TYPE,
format!(
"{name}: arg #{} must be 1-D array, got other shape",
position + 1
),
)),
}
}
fn expect_array2d(
&self,
name: &str,
position: usize,
) -> Result<&[Vec<f64>], ClosedFunctionError> {
match self {
ClosedArg::Array2D(v) => Ok(v.as_slice()),
ClosedArg::Scalar(_) | ClosedArg::Array(_) => Err(ClosedFunctionError::new(
codes::CLOSED_FUNCTION_ARG_TYPE,
format!(
"{name}: arg #{} must be 2-D array, got other shape",
position + 1
),
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ClosedValue {
Integer(i32),
Float(f64),
}
impl ClosedValue {
pub fn as_f64(self) -> f64 {
match self {
ClosedValue::Integer(i) => i as f64,
ClosedValue::Float(f) => f,
}
}
}
type ClosedHandler = fn(&str, &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError>;
const CLOSED_FUNCTIONS: &[(&str, ClosedHandler)] = &[
("datetime.year", cf_datetime_year),
("datetime.month", |name, args| {
datetime_field(name, args, |(_, m, _, _, _, _)| m)
}),
("datetime.day", |name, args| {
datetime_field(name, args, |(_, _, d, _, _, _)| d)
}),
("datetime.hour", |name, args| {
datetime_field(name, args, |(_, _, _, h, _, _)| h)
}),
("datetime.minute", |name, args| {
datetime_field(name, args, |(_, _, _, _, mi, _)| mi)
}),
("datetime.second", |name, args| {
datetime_field(name, args, |(_, _, _, _, _, s)| s)
}),
("datetime.day_of_year", cf_datetime_day_of_year),
("datetime.julian_day", cf_datetime_julian_day),
("datetime.is_leap_year", cf_datetime_is_leap_year),
("interp.searchsorted", cf_interp_searchsorted),
("interp.linear", cf_interp_linear),
("interp.bilinear", cf_interp_bilinear),
];
pub fn closed_function_names() -> &'static HashSet<String> {
static NAMES: OnceLock<HashSet<String>> = OnceLock::new();
NAMES.get_or_init(|| {
CLOSED_FUNCTIONS
.iter()
.map(|(n, _)| (*n).to_string())
.collect()
})
}
pub fn evaluate_closed_function(
name: &str,
args: &[ClosedArg],
) -> Result<ClosedValue, ClosedFunctionError> {
match CLOSED_FUNCTIONS.iter().find(|(n, _)| *n == name) {
Some((n, handler)) => handler(n, args),
None => Err(ClosedFunctionError::new(
codes::UNKNOWN_CLOSED_FUNCTION,
format!(
"`fn` name `{name}` is not in the v0.3.0 closed function registry \
(esm-spec §9.2). Adding a primitive requires a spec rev."
),
)),
}
}
fn cf_datetime_year(name: &str, args: &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 1)?;
let t = args[0].expect_scalar(name, 0)?;
let (y, _, _, _, _, _) = decompose_utc(t);
Ok(ClosedValue::Integer(check_i32(name, y as i64)?))
}
fn cf_datetime_day_of_year(
name: &str,
args: &[ClosedArg],
) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 1)?;
let t = args[0].expect_scalar(name, 0)?;
let (y, m, d, _, _, _) = decompose_utc(t);
Ok(ClosedValue::Integer(day_of_year(y, m, d) as i32))
}
fn cf_datetime_julian_day(
name: &str,
args: &[ClosedArg],
) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 1)?;
let t = args[0].expect_scalar(name, 0)?;
Ok(ClosedValue::Float(julian_day(t)))
}
fn cf_datetime_is_leap_year(
name: &str,
args: &[ClosedArg],
) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 1)?;
let t = args[0].expect_scalar(name, 0)?;
let (y, _, _, _, _, _) = decompose_utc(t);
Ok(ClosedValue::Integer(if is_leap_year(y) { 1 } else { 0 }))
}
fn cf_interp_searchsorted(
name: &str,
args: &[ClosedArg],
) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 2)?;
let x = args[0].expect_scalar(name, 0)?;
let xs = args[1].expect_array(name, 1)?;
Ok(ClosedValue::Integer(check_i32(
name,
searchsorted(name, x, xs)?,
)?))
}
fn cf_interp_linear(name: &str, args: &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 3)?;
let table = args[0].expect_array(name, 0)?;
let axis = args[1].expect_array(name, 1)?;
let x = args[2].expect_scalar(name, 2)?;
Ok(ClosedValue::Float(interp_linear(table, axis, x)?))
}
fn cf_interp_bilinear(name: &str, args: &[ClosedArg]) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 5)?;
let table = args[0].expect_array2d(name, 0)?;
let axis_x = args[1].expect_array(name, 1)?;
let axis_y = args[2].expect_array(name, 2)?;
let x = args[3].expect_scalar(name, 3)?;
let y = args[4].expect_scalar(name, 4)?;
Ok(ClosedValue::Float(interp_bilinear(
table, axis_x, axis_y, x, y,
)?))
}
fn datetime_field(
name: &str,
args: &[ClosedArg],
select: impl Fn((i32, u32, u32, u32, u32, u32)) -> u32,
) -> Result<ClosedValue, ClosedFunctionError> {
expect_arity(name, args, 1)?;
let t = args[0].expect_scalar(name, 0)?;
Ok(ClosedValue::Integer(select(decompose_utc(t)) as i32))
}
fn expect_arity(name: &str, args: &[ClosedArg], n: usize) -> Result<(), ClosedFunctionError> {
if args.len() != n {
return Err(ClosedFunctionError::new(
codes::CLOSED_FUNCTION_ARITY,
format!("{name} expects {n} argument(s), got {}", args.len()),
));
}
Ok(())
}
fn check_i32(name: &str, v: i64) -> Result<i32, ClosedFunctionError> {
if v < i32::MIN as i64 || v > i32::MAX as i64 {
return Err(ClosedFunctionError::new(
codes::CLOSED_FUNCTION_OVERFLOW,
format!("{name}: result {v} overflows Int32"),
));
}
Ok(v as i32)
}
#[inline]
fn floor_div_mod(t_utc: f64) -> (i64, f64) {
let day = (t_utc / 86400.0).floor();
let mut sec = t_utc - day * 86400.0;
if sec >= 86400.0 {
sec -= 86400.0;
} else if sec < 0.0 {
sec += 86400.0;
}
(day as i64, sec)
}
fn decompose_utc(t_utc: f64) -> (i32, u32, u32, u32, u32, u32) {
let (day_count, sec_in_day) = floor_div_mod(t_utc);
let (y, m, d) = civil_from_days(day_count);
let s = sec_in_day.trunc() as i64;
let hour = (s / 3600) as u32;
let minute = ((s % 3600) / 60) as u32;
let second = (s % 60) as u32;
(y, m, d, hour, minute, second)
}
fn civil_from_days(z: i64) -> (i32, u32, u32) {
let z = z + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = (yoe as i64) + era * 400;
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 };
(y as i32, m, d)
}
fn is_leap_year(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
}
fn day_of_year(y: i32, m: u32, d: u32) -> u32 {
const CUM: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let mut doy = CUM[(m - 1) as usize] + d;
if m > 2 && is_leap_year(y) {
doy += 1;
}
doy
}
fn julian_day(t_utc: f64) -> f64 {
let (day_count, sec_in_day) = floor_div_mod(t_utc);
let (y, m, d) = civil_from_days(day_count);
let y = y as i64;
let m = m as i64;
let d = d as i64;
let jdn = (1461 * (y + 4800 + (m - 14) / 12)) / 4 + (367 * (m - 2 - 12 * ((m - 14) / 12))) / 12
- (3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4
+ d
- 32075;
(jdn as f64) + (sec_in_day - 43200.0) / 86400.0
}
fn searchsorted(name: &str, x: f64, xs: &[f64]) -> Result<i64, ClosedFunctionError> {
let mut prev = f64::NAN;
for (i, v) in xs.iter().copied().enumerate() {
if v.is_nan() {
return Err(ClosedFunctionError::new(
codes::SEARCHSORTED_NAN_IN_TABLE,
format!(
"{name}: xs[{idx}] is NaN; NaN entries in xs are forbidden",
idx = i + 1
),
));
}
if i > 0 && v < prev {
return Err(ClosedFunctionError::new(
codes::SEARCHSORTED_NON_MONOTONIC,
format!(
"{name}: xs is not non-decreasing (xs[{idx}]={v} < xs[{prev_idx}]={prev})",
idx = i + 1,
prev_idx = i,
),
));
}
prev = v;
}
Ok(searchsorted_at(x, xs))
}
pub(crate) fn searchsorted_at(x: f64, xs: &[f64]) -> i64 {
let n = xs.len();
if n == 0 {
return 1;
}
if x.is_nan() {
return (n as i64) + 1;
}
for (i, v) in xs.iter().copied().enumerate() {
if v >= x {
return (i as i64) + 1;
}
}
(n as i64) + 1
}
fn interp_linear(table: &[f64], axis: &[f64], x: f64) -> Result<f64, ClosedFunctionError> {
validate_axis("interp.linear", "axis", axis)?;
if table.len() != axis.len() {
return Err(ClosedFunctionError::new(
codes::INTERP_AXIS_LENGTH_MISMATCH,
format!(
"interp.linear: len(table)={} != len(axis)={}",
table.len(),
axis.len()
),
));
}
Ok(interp_linear_at(table, axis, x))
}
pub(crate) fn interp_linear_at(table: &[f64], axis: &[f64], x: f64) -> f64 {
let n = axis.len();
if x <= axis[0] {
return table[0];
}
if x >= axis[n - 1] {
return table[n - 1];
}
let mut i = 0usize;
if x.is_nan() {
i = 0;
} else {
for k in 0..(n - 1) {
if axis[k] <= x && x < axis[k + 1] {
i = k;
break;
}
}
}
let w = (x - axis[i]) / (axis[i + 1] - axis[i]);
table[i] + w * (table[i + 1] - table[i])
}
fn interp_bilinear(
table: &[Vec<f64>],
axis_x: &[f64],
axis_y: &[f64],
x: f64,
y: f64,
) -> Result<f64, ClosedFunctionError> {
validate_axis("interp.bilinear", "axis_x", axis_x)?;
validate_axis("interp.bilinear", "axis_y", axis_y)?;
if table.len() != axis_x.len() {
return Err(ClosedFunctionError::new(
codes::INTERP_AXIS_LENGTH_MISMATCH,
format!(
"interp.bilinear: outer len(table)={} != len(axis_x)={}",
table.len(),
axis_x.len()
),
));
}
let ny = axis_y.len();
for (i, row) in table.iter().enumerate() {
if row.len() != ny {
return Err(ClosedFunctionError::new(
codes::INTERP_AXIS_LENGTH_MISMATCH,
format!(
"interp.bilinear: table row {row_idx} has len={got} but len(axis_y)={ny}",
row_idx = i + 1,
got = row.len(),
),
));
}
}
Ok(bilinear_at(|i, j| table[i][j], axis_x, axis_y, x, y))
}
pub(crate) fn interp_bilinear_at(
table: &[f64],
axis_x: &[f64],
axis_y: &[f64],
x: f64,
y: f64,
) -> f64 {
let ny = axis_y.len();
bilinear_at(|i, j| table[i * ny + j], axis_x, axis_y, x, y)
}
fn bilinear_at(
at: impl Fn(usize, usize) -> f64,
axis_x: &[f64],
axis_y: &[f64],
x: f64,
y: f64,
) -> f64 {
let nx = axis_x.len();
let ny = axis_y.len();
let x_q = if x <= axis_x[0] {
axis_x[0]
} else if x >= axis_x[nx - 1] {
axis_x[nx - 1]
} else {
x
};
let y_q = if y <= axis_y[0] {
axis_y[0]
} else if y >= axis_y[ny - 1] {
axis_y[ny - 1]
} else {
y
};
let i = locate_cell(axis_x, x_q);
let j = locate_cell(axis_y, y_q);
let wx = (x_q - axis_x[i]) / (axis_x[i + 1] - axis_x[i]);
let wy = (y_q - axis_y[j]) / (axis_y[j + 1] - axis_y[j]);
let row_j = at(i, j) + wx * (at(i + 1, j) - at(i, j));
let row_jp1 = at(i, j + 1) + wx * (at(i + 1, j + 1) - at(i, j + 1));
row_j + wy * (row_jp1 - row_j)
}
fn locate_cell(axis: &[f64], q: f64) -> usize {
let last = axis.len() - 2;
let mut i = 0usize;
for (k, v) in axis.iter().take(last + 1).enumerate() {
if *v <= q {
i = k;
} else {
break;
}
}
i
}
fn validate_axis(fn_name: &str, label: &str, axis: &[f64]) -> Result<(), ClosedFunctionError> {
if axis.len() < 2 {
return Err(ClosedFunctionError::new(
codes::INTERP_AXIS_TOO_SHORT,
format!(
"{fn_name}: {label} has {len} entries; need at least 2 (no interval to blend across)",
len = axis.len()
),
));
}
for (i, v) in axis.iter().copied().enumerate() {
if v.is_nan() {
return Err(ClosedFunctionError::new(
codes::INTERP_NAN_IN_AXIS,
format!(
"{fn_name}: {label}[{idx}] is NaN; NaN entries in axes are forbidden",
idx = i + 1
),
));
}
}
for w in axis.windows(2) {
if w[0] >= w[1] {
return Err(ClosedFunctionError::new(
codes::INTERP_NON_MONOTONIC_AXIS,
format!(
"{fn_name}: {label} is not strictly increasing (encountered {a} then {b})",
a = w[0],
b = w[1]
),
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: f64) -> ClosedArg {
ClosedArg::Scalar(v)
}
fn a(vs: &[f64]) -> ClosedArg {
ClosedArg::Array(vs.to_vec())
}
fn a2(rows: &[&[f64]]) -> ClosedArg {
ClosedArg::Array2D(rows.iter().map(|r| r.to_vec()).collect())
}
#[test]
fn unknown_name_diagnostic() {
let err = evaluate_closed_function("datetime.century", &[s(0.0)]).unwrap_err();
assert_eq!(err.code, "unknown_closed_function");
}
#[test]
fn closed_function_names_v030() {
let names = closed_function_names();
assert_eq!(names.len(), 12);
for n in [
"datetime.year",
"datetime.month",
"datetime.day",
"datetime.hour",
"datetime.minute",
"datetime.second",
"datetime.day_of_year",
"datetime.julian_day",
"datetime.is_leap_year",
"interp.searchsorted",
"interp.linear",
"interp.bilinear",
] {
assert!(names.contains(n), "missing {n}");
}
}
#[test]
fn epoch_year() {
let v = evaluate_closed_function("datetime.year", &[s(0.0)]).unwrap();
assert_eq!(v, ClosedValue::Integer(1970));
}
#[test]
fn pre_epoch_year() {
let v = evaluate_closed_function("datetime.year", &[s(-1.0)]).unwrap();
assert_eq!(v, ClosedValue::Integer(1969));
}
#[test]
fn leap_year_2000() {
let v = evaluate_closed_function("datetime.is_leap_year", &[s(951825600.0)]).unwrap();
assert_eq!(v, ClosedValue::Integer(1));
}
#[test]
fn not_leap_2100() {
let v = evaluate_closed_function("datetime.is_leap_year", &[s(4107542399.0)]).unwrap();
assert_eq!(v, ClosedValue::Integer(0));
}
#[test]
fn day_of_year_leap_dec31() {
let v = evaluate_closed_function("datetime.day_of_year", &[s(1735689599.0)]).unwrap();
assert_eq!(v, ClosedValue::Integer(366));
}
#[test]
fn searchsorted_exact_boundary() {
let v = evaluate_closed_function(
"interp.searchsorted",
&[s(2.0), a(&[1.0, 2.0, 3.0, 4.0, 5.0])],
)
.unwrap();
assert_eq!(v, ClosedValue::Integer(2));
}
#[test]
fn searchsorted_above_range() {
let v = evaluate_closed_function(
"interp.searchsorted",
&[s(10.0), a(&[1.0, 2.0, 3.0, 4.0, 5.0])],
)
.unwrap();
assert_eq!(v, ClosedValue::Integer(6));
}
#[test]
fn searchsorted_nan_x() {
let v = evaluate_closed_function(
"interp.searchsorted",
&[s(f64::NAN), a(&[1.0, 2.0, 3.0, 4.0, 5.0])],
)
.unwrap();
assert_eq!(v, ClosedValue::Integer(6));
}
#[test]
fn searchsorted_non_monotonic_rejects() {
let err = evaluate_closed_function(
"interp.searchsorted",
&[s(2.0), a(&[1.0, 3.0, 2.0, 4.0, 5.0])],
)
.unwrap_err();
assert_eq!(err.code, "searchsorted_non_monotonic");
}
#[test]
fn searchsorted_nan_in_xs_rejects() {
let err =
evaluate_closed_function("interp.searchsorted", &[s(2.0), a(&[1.0, f64::NAN, 3.0])])
.unwrap_err();
assert_eq!(err.code, "searchsorted_nan_in_table");
}
#[test]
fn searchsorted_duplicates_left_bias() {
let v = evaluate_closed_function(
"interp.searchsorted",
&[s(2.0), a(&[1.0, 2.0, 2.0, 2.0, 3.0])],
)
.unwrap();
assert_eq!(v, ClosedValue::Integer(2));
}
#[test]
fn interp_linear_exact_at_knot() {
let v = evaluate_closed_function(
"interp.linear",
&[
a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
s(2.0),
],
)
.unwrap();
assert_eq!(v.as_f64(), 40.0);
}
#[test]
fn interp_linear_midpoint_blend() {
let v = evaluate_closed_function(
"interp.linear",
&[
a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
s(0.5),
],
)
.unwrap();
assert_eq!(v.as_f64(), 15.0);
}
#[test]
fn interp_linear_below_clamps() {
let v = evaluate_closed_function(
"interp.linear",
&[a(&[3.0, 7.0]), a(&[10.0, 20.0]), s(-100.0)],
)
.unwrap();
assert_eq!(v.as_f64(), 3.0);
}
#[test]
fn interp_linear_above_clamps_exact_endpoint() {
let v = evaluate_closed_function(
"interp.linear",
&[
a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
s(99.0),
],
)
.unwrap();
assert_eq!(v.as_f64(), 160.0);
}
#[test]
fn interp_linear_nan_propagates() {
let v = evaluate_closed_function(
"interp.linear",
&[
a(&[10.0, 20.0, 40.0, 80.0, 160.0]),
a(&[0.0, 1.0, 2.0, 3.0, 4.0]),
s(f64::NAN),
],
)
.unwrap();
assert!(v.as_f64().is_nan());
}
#[test]
fn interp_linear_non_monotonic_rejects() {
let err = evaluate_closed_function(
"interp.linear",
&[
a(&[10.0, 20.0, 30.0, 40.0]),
a(&[0.0, 2.0, 1.0, 3.0]),
s(1.5),
],
)
.unwrap_err();
assert_eq!(err.code, "interp_non_monotonic_axis");
}
#[test]
fn interp_linear_equal_adjacent_rejects() {
let err = evaluate_closed_function(
"interp.linear",
&[
a(&[10.0, 20.0, 30.0, 40.0]),
a(&[0.0, 1.0, 1.0, 2.0]),
s(0.5),
],
)
.unwrap_err();
assert_eq!(err.code, "interp_non_monotonic_axis");
}
#[test]
fn interp_linear_length_mismatch_rejects() {
let err = evaluate_closed_function(
"interp.linear",
&[a(&[10.0, 20.0, 30.0, 40.0]), a(&[0.0, 1.0, 2.0]), s(1.0)],
)
.unwrap_err();
assert_eq!(err.code, "interp_axis_length_mismatch");
}
#[test]
fn interp_linear_nan_in_axis_rejects() {
let err = evaluate_closed_function(
"interp.linear",
&[
a(&[10.0, 20.0, 30.0, 40.0]),
a(&[0.0, f64::NAN, 2.0, 3.0]),
s(1.0),
],
)
.unwrap_err();
assert_eq!(err.code, "interp_nan_in_axis");
}
#[test]
fn interp_linear_axis_too_short_rejects() {
let err = evaluate_closed_function("interp.linear", &[a(&[42.0]), a(&[0.0]), s(0.0)])
.unwrap_err();
assert_eq!(err.code, "interp_axis_too_short");
}
#[test]
fn interp_bilinear_exact_corner() {
let v = evaluate_closed_function(
"interp.bilinear",
&[
a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
a(&[0.0, 1.0, 2.0]),
a(&[0.0, 10.0, 20.0]),
s(2.0),
s(20.0),
],
)
.unwrap();
assert_eq!(v.as_f64(), 22.0);
}
#[test]
fn interp_bilinear_center_blend() {
let v = evaluate_closed_function(
"interp.bilinear",
&[
a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
a(&[0.0, 1.0, 2.0]),
a(&[0.0, 10.0, 20.0]),
s(0.5),
s(5.0),
],
)
.unwrap();
assert_eq!(v.as_f64(), 5.5);
}
#[test]
fn interp_bilinear_clamps_to_corner() {
let v = evaluate_closed_function(
"interp.bilinear",
&[
a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
a(&[0.0, 1.0, 2.0]),
a(&[0.0, 10.0, 20.0]),
s(1000.0),
s(1000.0),
],
)
.unwrap();
assert_eq!(v.as_f64(), 22.0);
}
#[test]
fn interp_bilinear_nan_x_propagates() {
let v = evaluate_closed_function(
"interp.bilinear",
&[
a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
a(&[0.0, 1.0, 2.0]),
a(&[0.0, 10.0, 20.0]),
s(f64::NAN),
s(5.0),
],
)
.unwrap();
assert!(v.as_f64().is_nan());
}
#[test]
fn interp_bilinear_ragged_rows_reject() {
let err = evaluate_closed_function(
"interp.bilinear",
&[
a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0], &[20.0, 21.0, 22.0]]),
a(&[0.0, 1.0, 2.0]),
a(&[0.0, 10.0, 20.0]),
s(0.5),
s(5.0),
],
)
.unwrap_err();
assert_eq!(err.code, "interp_axis_length_mismatch");
}
#[test]
fn interp_bilinear_axis_x_length_mismatch_rejects() {
let err = evaluate_closed_function(
"interp.bilinear",
&[
a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
a(&[0.0, 1.0]),
a(&[0.0, 10.0, 20.0]),
s(0.5),
s(5.0),
],
)
.unwrap_err();
assert_eq!(err.code, "interp_axis_length_mismatch");
}
#[test]
fn interp_bilinear_non_monotonic_axis_rejects() {
let err = evaluate_closed_function(
"interp.bilinear",
&[
a2(&[&[0.0, 1.0, 2.0], &[10.0, 11.0, 12.0], &[20.0, 21.0, 22.0]]),
a(&[0.0, 2.0, 1.0]),
a(&[0.0, 10.0, 20.0]),
s(0.5),
s(5.0),
],
)
.unwrap_err();
assert_eq!(err.code, "interp_non_monotonic_axis");
}
}