use std::sync::LazyLock;
use regex::Regex;
use rust_decimal::Decimal;
use super::Call;
use crate::eval::convert::{boolean, single_value, string};
use crate::eval::error::EvalError;
use crate::quantity::{Quantity, Unit};
use crate::temporal::{Date, DateTime, Time};
use crate::value::{Collection, Item};
static INTEGER: LazyLock<Option<Regex>> = LazyLock::new(|| Regex::new(r"^(\+|-)?\d+$").ok());
static DECIMAL: LazyLock<Option<Regex>> =
LazyLock::new(|| Regex::new(r"^(\+|-)?\d+(\.\d+)?$").ok());
fn matches(form: &LazyLock<Option<Regex>>, text: &str) -> bool {
form.as_ref().is_some_and(|form| form.is_match(text))
}
pub(super) fn call<'a>(call: &Call<'_, '_, 'a>) -> Result<Option<Collection<'a>>, EvalError> {
if call.name == "iif" {
return iif(call).map(Some);
}
let Some((target, test)) = target(call.name) else {
return Ok(None);
};
let arity = if target == Target::Quantity {
(0, 1, "0 or 1")
} else {
(0, 0, "no")
};
call.arity(arity.0, arity.1, arity.2)?;
let Some(item) = single_value(call.model(), &call.input, call.name)? else {
return Ok(Some(Vec::new()));
};
let unit = match call.arg(0)? {
Some(unit) => string(call.model(), &unit, call.name)?,
None => None,
};
let converted = convert(&item, target, unit.as_deref());
Ok(Some(if test {
vec![Item::Boolean(converted.is_some())]
} else {
converted.into_iter().collect()
}))
}
fn iif<'a>(call: &Call<'_, '_, 'a>) -> Result<Collection<'a>, EvalError> {
call.arity(2, 3, "2 or 3")?;
let focus = match call.input.as_slice() {
[] => None,
[item] => Some(item.clone()),
items => return Err(EvalError::singleton("iif", items.len())),
};
let frame = crate::eval::Frame {
focus: focus.into_iter().collect(),
index: call.frame.index,
total: call.frame.total.clone(),
};
let evaluate = |index: usize| {
call.args
.get(index)
.map_or_else(|| Ok(Vec::new()), |arg| call.engine.eval(arg, &frame))
};
if boolean(call.model(), &evaluate(0)?, "iif")? == Some(true) {
evaluate(1)
} else if call.args.len() == 3 {
evaluate(2)
} else {
Ok(Vec::new())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Target {
Boolean,
Integer,
Decimal,
Date,
DateTime,
Time,
Quantity,
String,
}
fn target(name: &str) -> Option<(Target, bool)> {
let (rest, test) = match name.strip_prefix("convertsTo") {
Some(rest) => (rest, true),
None => (name.strip_prefix("to")?, false),
};
let target = match rest {
"Boolean" => Target::Boolean,
"Integer" => Target::Integer,
"Decimal" => Target::Decimal,
"Date" => Target::Date,
"DateTime" => Target::DateTime,
"Time" => Target::Time,
"Quantity" => Target::Quantity,
"String" => Target::String,
_ => return None,
};
Some((target, test))
}
fn convert<'a>(item: &Item<'_>, target: Target, unit: Option<&str>) -> Option<Item<'a>> {
match target {
Target::Boolean => to_boolean(item).map(Item::Boolean),
Target::Integer => match item {
Item::Integer(value) => Some(Item::Integer(*value)),
Item::Boolean(value) => Some(Item::Integer(i64::from(*value))),
Item::String(text) if matches(&INTEGER, text) => text
.trim_start_matches('+')
.parse::<i32>()
.ok()
.map(|value| Item::Integer(i64::from(value))),
_ => None,
},
Target::Decimal => match item {
Item::Integer(value) => Some(Item::Decimal(Decimal::from(*value))),
Item::Decimal(value) => Some(Item::Decimal(*value)),
Item::Boolean(value) => {
Some(Item::Decimal(Decimal::new(if *value { 10 } else { 0 }, 1)))
}
Item::String(text) if matches(&DECIMAL, text) => {
text.trim_start_matches('+').parse().ok().map(Item::Decimal)
}
_ => None,
},
Target::Date => match item {
Item::Date(value) => Some(Item::Date(*value)),
Item::DateTime(value) => Some(Item::Date(value.date())),
Item::String(text) => Date::parse(text).map(Item::Date),
_ => None,
},
Target::DateTime => match item {
Item::DateTime(value) => Some(Item::DateTime(*value)),
Item::Date(value) => Some(Item::DateTime(value.to_date_time())),
Item::String(text) => DateTime::parse(text).map(Item::DateTime),
_ => None,
},
Target::Time => match item {
Item::Time(value) => Some(Item::Time(*value)),
Item::String(text) => Time::parse(text).map(Item::Time),
_ => None,
},
Target::Quantity => to_quantity(item)
.and_then(|quantity| in_unit(quantity, unit))
.map(Item::Quantity),
Target::String => match item {
Item::Node(_) | Item::Type(_) => None,
other => Some(Item::String(other.to_string())),
},
}
}
fn to_boolean(item: &Item<'_>) -> Option<bool> {
match item {
Item::Boolean(value) => Some(*value),
Item::Integer(1) => Some(true),
Item::Integer(0) => Some(false),
Item::Decimal(value) if *value == Decimal::ONE => Some(true),
Item::Decimal(value) if value.is_zero() => Some(false),
Item::String(text) => match text.to_lowercase().as_str() {
"true" | "t" | "yes" | "y" | "1" | "1.0" => Some(true),
"false" | "f" | "no" | "n" | "0" | "0.0" => Some(false),
_ => None,
},
_ => None,
}
}
fn to_quantity(item: &Item<'_>) -> Option<Quantity> {
match item {
Item::Integer(value) => Some(Quantity::new(Decimal::from(*value), Unit::one())),
Item::Decimal(value) => Some(Quantity::new(*value, Unit::one())),
Item::Quantity(value) => Some(value.clone()),
Item::Boolean(value) => Some(Quantity::new(
Decimal::new(if *value { 10 } else { 0 }, 1),
Unit::one(),
)),
Item::String(text) => Quantity::parse(text),
_ => None,
}
}
fn in_unit(quantity: Quantity, unit: Option<&str>) -> Option<Quantity> {
match unit {
Some(unit) => quantity.convert(&Unit::from_code(String::from(unit))),
None => Some(quantity),
}
}