use crate::types::UnitMap;
use rhai::{Array, Dynamic, EvalAltResult, Map, Position};
use std::{collections::BTreeMap, str::FromStr};
use zfuel::fuel::ZFuel;
pub fn parse_float(s: String) -> f64 {
s.parse::<f64>().unwrap_or(0.0)
}
pub fn to_num(value: Dynamic) -> Result<f64, Box<EvalAltResult>> {
let type_name = value.type_name();
if let Ok(f) = value.as_float() {
if !f.is_finite() {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("to_num: {f} is not a finite number").into(),
Position::NONE,
)));
}
return Ok(f);
}
if let Ok(i) = value.as_int() {
return Ok(i as f64);
}
let shown = value.to_string();
if value.into_immutable_string().is_ok() {
return match shown.parse::<f64>() {
Ok(f) if f.is_finite() => Ok(f),
_ => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("to_num: \"{shown}\" is not a finite number").into(),
Position::NONE,
))),
};
}
Err(Box::new(EvalAltResult::ErrorRuntime(
format!("to_num: expected a number or numeric string, got {type_name} ({shown})").into(),
Position::NONE,
)))
}
pub fn add_units(a: Map, b: Map) -> Result<Map, Box<EvalAltResult>> {
let mut a = parse_amount(a)?;
let b = parse_amount(b)?;
a.add(b).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.to_string().into(),
Position::NONE,
))
})?;
Ok(a.to_map())
}
pub fn sub_units(a: Map, b: Map) -> Result<Map, Box<EvalAltResult>> {
let mut a = parse_amount(a)?;
let b = parse_amount(b)?;
a.sub(b).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.to_string().into(),
Position::NONE,
))
})?;
Ok(a.to_map())
}
pub fn extract_indexes(amount: Map, indexes: Array) -> Result<Map, Box<EvalAltResult>> {
let units = parse_amount(amount)?;
let indexes: Vec<String> = indexes.into_iter().map(|index| index.to_string()).collect();
let mut extracted_amount = Map::new();
for (key, value) in units.into_iter() {
if indexes.contains(&key) {
extracted_amount.insert(key.into(), value.to_string().into());
}
}
Ok(extracted_amount)
}
fn parse_amount(amount: Map) -> Result<UnitMap, Box<EvalAltResult>> {
let mut units = BTreeMap::new();
for (key, value) in amount.iter() {
units.insert(
key.to_string(),
ZFuel::from_str(value.to_string().as_str()).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!(
"Failed to parse unit amount: {}",
value.to_string().as_str()
)
.into(),
Position::NONE,
))
})?,
);
}
Ok(UnitMap::load(units))
}
pub fn add_fuel(a: String, b: String) -> Result<String, Box<EvalAltResult>> {
let a = ZFuel::from_str(&a).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to parse unit a amount: {}", a).into(),
Position::NONE,
))
})?;
let b = ZFuel::from_str(&b).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to parse fuel b amount: {}", b).into(),
Position::NONE,
))
})?;
let c = (a + b).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to add fuel amounts: {} + {}", a, b).into(),
Position::NONE,
))
})?;
Ok(c.to_string())
}
pub fn sub_fuel(a: String, b: String) -> Result<String, Box<EvalAltResult>> {
let a = ZFuel::from_str(&a).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to parse fuel amount: {}", a).into(),
Position::NONE,
))
})?;
let b = ZFuel::from_str(&b).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to parse fuel amount: {}", b).into(),
Position::NONE,
))
})?;
let c = (a - b).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to subtract fuel amounts: {} - {}", a, b).into(),
Position::NONE,
))
})?;
Ok(c.to_string())
}
pub fn is_greater_than_eq(a: String, b: String) -> Result<bool, Box<EvalAltResult>> {
let a = ZFuel::from_str(&a).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
"Failed to parse fuel amount".into(),
Position::NONE,
))
})?;
let b = ZFuel::from_str(&b).map_err(|_| {
Box::new(EvalAltResult::ErrorRuntime(
"Failed to parse fuel amount".into(),
Position::NONE,
))
})?;
Ok(a >= b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_num_accepts_both_json_shapes() {
assert_eq!(to_num(Dynamic::from(0.5_f64)).unwrap(), 0.5);
assert_eq!(to_num(Dynamic::from(7_i64)).unwrap(), 7.0);
assert_eq!(to_num(Dynamic::from("0.5".to_string())).unwrap(), 0.5);
assert_eq!(to_num(Dynamic::from("-2.99".to_string())).unwrap(), -2.99);
assert_eq!(to_num(Dynamic::from("0".to_string())).unwrap(), 0.0);
}
#[test]
fn to_num_names_the_offending_value_instead_of_silent_zero() {
let err = to_num(Dynamic::from("O.5".to_string()))
.unwrap_err()
.to_string();
assert!(err.contains("O.5"), "error must name the value: {err}");
assert!(to_num(Dynamic::from(String::new())).is_err());
}
#[test]
fn to_num_refuses_non_numeric_types_and_non_finite() {
let err = to_num(Dynamic::from(true)).unwrap_err().to_string();
assert!(err.contains("bool"), "error must name the type: {err}");
assert!(to_num(Dynamic::UNIT).is_err());
assert!(to_num(Dynamic::from("NaN".to_string())).is_err());
assert!(to_num(Dynamic::from("inf".to_string())).is_err());
assert!(to_num(Dynamic::from(f64::NAN)).is_err());
}
#[test]
fn test_parse_float_positive() {
assert_eq!(parse_float("2.99".to_string()), 2.99);
assert_eq!(parse_float("100.5".to_string()), 100.5);
assert_eq!(parse_float("0.5".to_string()), 0.5);
}
#[test]
fn test_parse_float_negative() {
assert_eq!(parse_float("-2.99".to_string()), -2.99);
assert_eq!(parse_float("-100.5".to_string()), -100.5);
assert_eq!(parse_float("-0.5".to_string()), -0.5);
}
#[test]
fn test_parse_float_integers() {
assert_eq!(parse_float("42".to_string()), 42.0);
assert_eq!(parse_float("-42".to_string()), -42.0);
assert_eq!(parse_float("0".to_string()), 0.0);
}
#[test]
fn test_parse_float_leading_decimal() {
assert_eq!(parse_float(".99".to_string()), 0.99);
assert_eq!(parse_float(".5".to_string()), 0.5);
}
#[test]
fn test_parse_float_invalid() {
assert_eq!(parse_float("invalid".to_string()), 0.0);
assert_eq!(parse_float("".to_string()), 0.0);
assert_eq!(parse_float("abc123".to_string()), 0.0);
}
#[test]
fn test_parse_float_edge_cases() {
assert_eq!(parse_float("0.0".to_string()), 0.0);
assert_eq!(parse_float("-0.0".to_string()), -0.0);
assert_eq!(parse_float(" 2.99 ".to_string()), 0.0); }
#[test]
fn test_parse_float_with_trim() {
let with_whitespace = " 2.99 ";
assert_eq!(parse_float(with_whitespace.trim().to_string()), 2.99);
let with_leading_space = " .99";
assert_eq!(parse_float(with_leading_space.trim().to_string()), 0.99);
}
#[test]
fn test_parse_float_substring_scenario() {
let negative_amount = "-2.99";
let positive_amount = "2.99";
let result_neg = parse_float(negative_amount[1..].to_string());
assert_eq!(result_neg, 2.99);
let result_pos = parse_float(positive_amount[1..].to_string());
assert_eq!(result_pos, 0.99); }
}