use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
use formualizer_common::{ExcelError, LiteralValue};
use std::sync::LazyLock;
pub const EPSILON_NEAR_ZERO: f64 = 1e-12;
pub fn aggregate_result(n: f64) -> LiteralValue {
if n.is_finite() {
LiteralValue::Number(n)
} else {
LiteralValue::Error(ExcelError::new_num())
}
}
pub fn coerce_num(value: &LiteralValue) -> Result<f64, ExcelError> {
crate::coercion::to_number_lenient(value)
}
pub fn unary_numeric_arg<'a, 'b>(
args: &'a [crate::traits::ArgumentHandle<'a, 'b>],
) -> Result<f64, ExcelError> {
if args.len() != 1 {
return Err(ExcelError::new_value()
.with_message(format!("Expected 1 argument, got {}", args.len())));
}
let v = args[0].value()?.into_literal();
match v {
LiteralValue::Error(e) => Err(e),
other => coerce_num(&other),
}
}
pub fn binary_numeric_args<'a, 'b>(
args: &'a [crate::traits::ArgumentHandle<'a, 'b>],
) -> Result<(f64, f64), ExcelError> {
if args.len() != 2 {
return Err(ExcelError::new_value()
.with_message(format!("Expected 2 arguments, got {}", args.len())));
}
let a = args[0].value()?.into_literal();
let b = args[1].value()?.into_literal();
let a_num = match a {
LiteralValue::Error(e) => return Err(e),
other => coerce_num(&other)?,
};
let b_num = match b {
LiteralValue::Error(e) => return Err(e),
other => coerce_num(&other)?,
};
Ok((a_num, b_num))
}
fn calc_from_literal<'b>(
v: LiteralValue,
date_system: crate::engine::DateSystem,
) -> crate::traits::CalcValue<'b> {
match v {
LiteralValue::Array(rows) => crate::traits::CalcValue::Range(
crate::engine::range_view::RangeView::from_owned_rows(rows, date_system),
),
other => crate::traits::CalcValue::Scalar(other),
}
}
pub(crate) const GENERATED_ARRAY_MAX_ROWS: i64 = 1_048_576;
pub(crate) const GENERATED_ARRAY_MAX_COLS: i64 = 16_384;
pub(crate) const GENERATED_ARRAY_MAX_CELLS: i64 = 1 << 24;
pub(crate) fn generated_array_too_large(rows: i64, cols: i64) -> Option<ExcelError> {
if rows > GENERATED_ARRAY_MAX_ROWS || cols > GENERATED_ARRAY_MAX_COLS {
return Some(ExcelError::new(formualizer_common::ExcelErrorKind::Num));
}
match rows.checked_mul(cols) {
Some(total) if total <= GENERATED_ARRAY_MAX_CELLS => None,
_ => Some(ExcelError::new(formualizer_common::ExcelErrorKind::Num)),
}
}
pub(crate) fn materialized_shape_too_large(shape: (usize, usize)) -> Option<ExcelError> {
match (i64::try_from(shape.0), i64::try_from(shape.1)) {
(Ok(rows), Ok(cols)) => generated_array_too_large(rows, cols),
_ => Some(ExcelError::new(formualizer_common::ExcelErrorKind::Num)),
}
}
pub(crate) const CANCEL_POLL_CELLS: usize = 4096;
pub(crate) struct CancelPoll<'p> {
is_cancelled: &'p dyn Fn() -> bool,
since_poll: usize,
}
impl<'p> CancelPoll<'p> {
pub(crate) fn new(is_cancelled: &'p dyn Fn() -> bool) -> Self {
Self {
is_cancelled,
since_poll: CANCEL_POLL_CELLS,
}
}
#[inline]
pub(crate) fn advance(&mut self, cells: usize) -> Result<(), ExcelError> {
if self.since_poll >= CANCEL_POLL_CELLS {
self.since_poll = 0;
if (self.is_cancelled)() {
return Err(ExcelError::new(
formualizer_common::ExcelErrorKind::Cancelled,
));
}
}
self.since_poll = self.since_poll.saturating_add(cells);
Ok(())
}
}
pub(crate) enum Grid<'b> {
Range(crate::engine::range_view::RangeView<'b>),
Array(Vec<Vec<LiteralValue>>),
Scalar(LiteralValue),
}
impl<'b> Grid<'b> {
pub(crate) fn shape(&self) -> (usize, usize) {
match self {
Grid::Range(rv) => rv.dims(),
Grid::Array(arr) => (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0)),
Grid::Scalar(_) => (1, 1),
}
}
pub(crate) fn get(&self, r: usize, c: usize) -> LiteralValue {
match self {
Grid::Range(rv) => rv.get_cell(r, c),
Grid::Array(arr) => arr
.get(r)
.and_then(|row| row.get(c))
.cloned()
.unwrap_or(LiteralValue::Empty),
Grid::Scalar(v) => v.clone(),
}
}
}
pub fn unary_numeric_elementwise<'a, 'b, F>(
args: &'a [crate::traits::ArgumentHandle<'a, 'b>],
ctx: &dyn crate::traits::FunctionContext<'b>,
mut f: F,
) -> Result<crate::traits::CalcValue<'b>, ExcelError>
where
F: FnMut(f64) -> Result<LiteralValue, ExcelError>,
{
if args.len() != 1 {
return Err(ExcelError::new_value()
.with_message(format!("Expected 1 argument, got {}", args.len())));
}
let shape = if let Ok(rv) = args[0].range_view() {
rv.dims()
} else if let Ok(cv) = args[0].value() {
match cv.into_literal() {
LiteralValue::Array(arr) => (arr.len(), arr.first().map(|r| r.len()).unwrap_or(0)),
_ => (1, 1),
}
} else {
(1, 1)
};
if shape != (1, 1) {
let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(shape.0);
if let Ok(view) = args[0].range_view() {
view.for_each_row(&mut |row| {
let mut out_row: Vec<LiteralValue> = Vec::with_capacity(row.len());
for cell in row.iter() {
let num_opt = match cell {
LiteralValue::Error(e) => return Err(e.clone()),
other => {
crate::coercion::to_number_lenient_with_locale(other, &ctx.locale())
.ok()
}
};
match num_opt {
Some(n) => out_row.push(f(n)?),
None => out_row.push(LiteralValue::Error(
ExcelError::new_value()
.with_message("Element is not coercible to number"),
)),
}
}
out.push(out_row);
Ok(())
})?;
} else {
let v = args[0].value()?.into_literal();
let LiteralValue::Array(arr) = v else {
let x = unary_numeric_arg(args)?;
return Ok(calc_from_literal(f(x)?, ctx.date_system()));
};
for row in arr {
let mut out_row: Vec<LiteralValue> = Vec::with_capacity(row.len());
for cell in row {
let num_opt = match &cell {
LiteralValue::Error(e) => return Err(e.clone()),
other => {
crate::coercion::to_number_lenient_with_locale(other, &ctx.locale())
.ok()
}
};
match num_opt {
Some(n) => out_row.push(f(n)?),
None => out_row.push(LiteralValue::Error(
ExcelError::new_value()
.with_message("Element is not coercible to number"),
)),
}
}
out.push(out_row);
}
}
return Ok(calc_from_literal(
LiteralValue::Array(out),
ctx.date_system(),
));
}
let x = unary_numeric_arg(args)?;
Ok(calc_from_literal(f(x)?, ctx.date_system()))
}
pub fn binary_numeric_elementwise<'a, 'b, F>(
args: &'a [crate::traits::ArgumentHandle<'a, 'b>],
ctx: &dyn crate::traits::FunctionContext<'b>,
mut f: F,
) -> Result<crate::traits::CalcValue<'b>, ExcelError>
where
F: FnMut(f64, f64) -> Result<LiteralValue, ExcelError>,
{
if args.len() != 2 {
return Err(ExcelError::new_value()
.with_message(format!("Expected 2 arguments, got {}", args.len())));
}
use crate::broadcast::{broadcast_shape, project_index};
fn to_grid<'a, 'b>(ah: &crate::traits::ArgumentHandle<'a, 'b>) -> Result<Grid<'b>, ExcelError> {
if let Ok(rv) = ah.range_view() {
return Ok(Grid::Range(rv));
}
let v = ah.value()?.into_literal();
Ok(match v {
LiteralValue::Array(arr) => Grid::Array(arr),
other => Grid::Scalar(other),
})
}
let g0 = to_grid(&args[0])?;
let g1 = to_grid(&args[1])?;
let s0 = g0.shape();
let s1 = g1.shape();
let target = broadcast_shape(&[s0, s1])?;
if target != (1, 1) {
let mut out: Vec<Vec<LiteralValue>> = Vec::with_capacity(target.0);
for r in 0..target.0 {
let mut out_row = Vec::with_capacity(target.1);
for c in 0..target.1 {
let (r0, c0) = project_index((r, c), s0);
let (r1, c1) = project_index((r, c), s1);
let lv0 = g0.get(r0, c0);
let lv1 = g1.get(r1, c1);
let n0 = match &lv0 {
LiteralValue::Error(e) => return Err(e.clone()),
other => {
crate::coercion::to_number_lenient_with_locale(other, &ctx.locale()).ok()
}
};
let n1 = match &lv1 {
LiteralValue::Error(e) => return Err(e.clone()),
other => {
crate::coercion::to_number_lenient_with_locale(other, &ctx.locale()).ok()
}
};
let out_cell = match (n0, n1) {
(Some(a), Some(b)) => f(a, b)?,
_ => LiteralValue::Error(
ExcelError::new_value()
.with_message("Elements are not coercible to numbers"),
),
};
out_row.push(out_cell);
}
out.push(out_row);
}
return Ok(calc_from_literal(
LiteralValue::Array(out),
ctx.date_system(),
));
}
let (a, b) = binary_numeric_args(args)?;
Ok(calc_from_literal(f(a, b)?, ctx.date_system()))
}
pub fn sanitize_numeric_result(n: f64) -> Result<f64, ExcelError> {
crate::coercion::sanitize_numeric(n)
}
pub fn coerce_text_to_number_maybe(value: &LiteralValue) -> Option<f64> {
match value {
LiteralValue::Text(_) => crate::coercion::to_number_lenient(value).ok(),
_ => None,
}
}
pub fn round_to_precision(n: f64, digits: i32) -> f64 {
if digits <= 0 {
return n.round();
}
let factor = 10f64.powi(digits);
(n * factor).round() / factor
}
pub fn collapse_if_scalar(
rows: Vec<Vec<LiteralValue>>,
date_system: crate::engine::DateSystem,
) -> crate::traits::CalcValue<'static> {
if rows.len() == 1 && rows[0].len() == 1 {
crate::traits::CalcValue::Scalar(rows[0][0].clone())
} else {
crate::traits::CalcValue::Range(crate::engine::range_view::RangeView::from_owned_rows(
rows,
date_system,
))
}
}
pub fn criteria_match(pred: &crate::args::CriteriaPredicate, v: &LiteralValue) -> bool {
use crate::args::CriteriaPredicate as P;
match pred {
P::Eq(t) => values_equal_invariant(t, v),
P::Ne(t) => !values_equal_invariant(t, v),
P::Gt(n) => criteria_number(v).is_some_and(|x| x > *n),
P::Ge(n) => criteria_number(v).is_some_and(|x| x >= *n),
P::Lt(n) => criteria_number(v).is_some_and(|x| x < *n),
P::Le(n) => criteria_number(v).is_some_and(|x| x <= *n),
P::TextLike {
pattern,
case_insensitive,
} => text_like_match(pattern, *case_insensitive, v),
P::NotTextLike {
pattern,
case_insensitive,
} => !text_like_match(pattern, *case_insensitive, v),
P::TextGt(t) => text_compare(v, t).is_some_and(|o| o.is_gt()),
P::TextGe(t) => text_compare(v, t).is_some_and(|o| o.is_ge()),
P::TextLt(t) => text_compare(v, t).is_some_and(|o| o.is_lt()),
P::TextLe(t) => text_compare(v, t).is_some_and(|o| o.is_le()),
P::IsBlank => matches!(v, LiteralValue::Empty),
P::NotBlank => !matches!(v, LiteralValue::Empty),
P::IsNumber => value_to_number(v).is_ok(),
P::IsText => matches!(v, LiteralValue::Text(_)),
P::IsLogical => matches!(v, LiteralValue::Boolean(_)),
}
}
fn value_to_number(v: &LiteralValue) -> Result<f64, ExcelError> {
crate::coercion::to_number_lenient(v)
}
fn criteria_number(v: &LiteralValue) -> Option<f64> {
match v {
LiteralValue::Boolean(_) => None,
other => other.as_serial_number(),
}
}
fn text_compare(v: &LiteralValue, operand: &str) -> Option<std::cmp::Ordering> {
match v {
LiteralValue::Text(t) => Some(t.to_lowercase().cmp(&operand.to_lowercase())),
_ => None,
}
}
fn values_equal_invariant(a: &LiteralValue, b: &LiteralValue) -> bool {
match (a, b) {
(LiteralValue::Number(x), LiteralValue::Number(y)) => (x - y).abs() < 1e-12,
(LiteralValue::Int(x), LiteralValue::Int(y)) => x == y,
(LiteralValue::Boolean(x), LiteralValue::Boolean(y)) => x == y,
(LiteralValue::Text(x), LiteralValue::Text(y)) => x.to_lowercase() == y.to_lowercase(),
(LiteralValue::Text(x), LiteralValue::Empty) if x.is_empty() => true,
(LiteralValue::Empty, LiteralValue::Text(y)) if y.is_empty() => true,
(LiteralValue::Empty, LiteralValue::Empty) => true,
(x, y) if criteria_number(x).is_some() && criteria_number(y).is_some() => {
criteria_number(x)
.zip(criteria_number(y))
.is_some_and(|(sx, sy)| (sx - sy).abs() < 1e-12)
}
(LiteralValue::Number(_) | LiteralValue::Int(_), LiteralValue::Text(_)) => {
criteria_number(a)
.zip(value_to_number(b).ok())
.is_some_and(|(x, y)| (x - y).abs() < 1e-12)
}
(LiteralValue::Text(_), LiteralValue::Number(_) | LiteralValue::Int(_)) => {
values_equal_invariant(b, a)
}
_ => false,
}
}
fn text_like_match(pattern: &str, case_insensitive: bool, v: &LiteralValue) -> bool {
let s = match v {
LiteralValue::Text(t) => t.as_str(),
_ => return false,
};
let (pat, text) = if case_insensitive {
(pattern.to_lowercase(), s.to_lowercase())
} else {
(pattern.to_string(), s.to_string())
};
if !pat.contains('?') && !pat.contains('~') {
if pat.ends_with('*') && !pat[..pat.len() - 1].contains('*') {
return text.starts_with(&pat[..pat.len() - 1]);
}
if pat.starts_with('*') && !pat[1..].contains('*') {
return text.ends_with(&pat[1..]);
}
if pat.starts_with('*') && pat.ends_with('*') && !pat[1..pat.len() - 1].contains('*') {
return text.contains(&pat[1..pat.len() - 1]);
}
if !pat.contains('*') {
return text == pat;
}
}
wildcard_match(&pat, &text)
}
fn wildcard_match(pat: &str, text: &str) -> bool {
wildcard_match_units(pat.as_bytes(), text.as_bytes(), false)
}
pub(crate) fn wildcard_match_units<T: Copy + Eq + From<u8>>(
pattern: &[T],
text: &[T],
prefix: bool,
) -> bool {
let star = T::from(b'*');
let question = T::from(b'?');
let tilde = T::from(b'~');
let (mut p, mut t) = (0, 0);
let mut retry = None;
loop {
if p == pattern.len() && (prefix || t == text.len()) {
return true;
}
if let Some(&unit) = pattern.get(p) {
let escaped = unit == tilde
&& pattern
.get(p + 1)
.is_some_and(|&next| next == star || next == question || next == tilde);
if !escaped && unit == star {
p += 1;
retry = Some((p, t));
continue;
}
let literal = if escaped { pattern[p + 1] } else { unit };
if t < text.len() && ((!escaped && unit == question) || literal == text[t]) {
p += if escaped { 2 } else { 1 };
t += 1;
continue;
}
}
match retry {
Some((after_star, consumed)) if consumed < text.len() => {
t = consumed + 1;
p = after_star;
retry = Some((after_star, t));
}
_ => return false,
}
}
}
pub static ARG_ANY_ONE: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| vec![ArgSchema::any()]);
pub static ARG_ANY_TWO: LazyLock<Vec<ArgSchema>> =
LazyLock::new(|| vec![ArgSchema::any(), ArgSchema::any()]);
pub static ARG_NUM_LENIENT_ONE: LazyLock<Vec<ArgSchema>> =
LazyLock::new(|| vec![{ ArgSchema::number_lenient_scalar() }]);
pub static ARG_NUM_LENIENT_TWO: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![{ ArgSchema::number_lenient_scalar() }, {
ArgSchema::number_lenient_scalar()
}]
});
pub static ARG_RANGE_NUM_LENIENT_ONE: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
vec![{
let mut s = ArgSchema::number_lenient_scalar();
s.shape = ShapeKind::Range;
s.coercion = CoercionPolicy::NumberLenientText;
s
}]
});