diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df6f3b20..06631443 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,8 @@ jobs: - "--features no_optimize" - "--features no_float" - "--features f32_float" + - "--features decimal" + - "--features no_float,decimal" - "--tests --features only_i32" - "--features only_i64" - "--features no_index" diff --git a/Cargo.toml b/Cargo.toml index fd71d97c..b4655469 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ no_float = [] # no floating-point f32_float = [] # set FLOAT=f32 only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types +decimal = [ "rust_decimal" ] # add the Decimal number type no_index = [] # no arrays and indexing no_object = [] # no custom objects no_function = [ "no_closure" ] # no script-defined functions (meaning no closures) @@ -44,7 +45,7 @@ no_closure = [] # no automatic sharing and capture of anonymous functions to no_module = [] # no modules internals = [] # expose internal data structures unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers. -metadata = [ "serde", "serde_json"] # enables exporting functions metadata to JSON +metadata = [ "serde", "serde_json" ] # enables exporting functions metadata to JSON # compiling for no-std no_std = [ "smallvec/union", "num-traits/libm", "hashbrown", "core-error", "libm", "ahash/compile-time-rng" ] @@ -98,6 +99,11 @@ version = "0.2" default_features = false optional = true +[dependencies.rust_decimal] +version = "1.10" +default_features = false +optional = true + [target.'cfg(target_arch = "wasm32")'.dependencies] instant= { version = "0.1" } # WASM implementation of std::time::Instant diff --git a/RELEASES.md b/RELEASES.md index 18f3ffd1..0d01c4e7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -19,11 +19,13 @@ Breaking changes * trigonometry functions now take radians and return radians instead of degrees * `Dynamic::into_shared` is no longer available under `no_closure`. It used to panic. * `Token::is_operator` is renamed to `Token::is_symbol`. +* `AST::clone_functions_only_filtered`, `AST::merge_filtered`, `AST::combine_filtered` and `AST::retain_functions` now take `Fn` instead of `FnMut` as the filter predicate. New features ------------ * Scientific notation is supported for floating-point number literals. +* A new feature, `decimal`, enables the [`Decimal`](https://crates.io/crates/rust_decimal) data type. When both `no_float` and `decimal` features are enabled, floating-point literals parse to `Decimal`. Enhancements ------------ diff --git a/src/ast.rs b/src/ast.rs index 0e4e4ee5..3ebf3373 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -21,7 +21,7 @@ use crate::{ }; #[cfg(not(feature = "no_float"))] -use crate::FLOAT; +use crate::{stdlib::str::FromStr, FLOAT}; #[cfg(not(feature = "no_index"))] use crate::Array; @@ -345,10 +345,10 @@ impl AST { #[inline(always)] pub fn clone_functions_only_filtered( &self, - mut filter: impl FnMut(FnNamespace, FnAccess, bool, &str, usize) -> bool, + filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> Self { let mut functions: Module = Default::default(); - functions.merge_filtered(&self.functions, &mut filter); + functions.merge_filtered(&self.functions, &filter); Self { source: self.source.clone(), statements: Default::default(), @@ -530,7 +530,7 @@ impl AST { pub fn merge_filtered( &self, other: &Self, - mut filter: impl FnMut(FnNamespace, FnAccess, bool, &str, usize) -> bool, + filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> Self { let Self { statements, @@ -552,7 +552,7 @@ impl AST { let source = other.source.clone().or_else(|| self.source.clone()); let mut functions = functions.as_ref().clone(); - functions.merge_filtered(&other.functions, &mut filter); + functions.merge_filtered(&other.functions, &filter); if let Some(source) = source { Self::new_with_source(ast, functions, source) @@ -615,11 +615,11 @@ impl AST { pub fn combine_filtered( &mut self, other: Self, - mut filter: impl FnMut(FnNamespace, FnAccess, bool, &str, usize) -> bool, + filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> &mut Self { self.statements.extend(other.statements.into_iter()); if !other.functions.is_empty() { - shared_make_mut(&mut self.functions).merge_filtered(&other.functions, &mut filter); + shared_make_mut(&mut self.functions).merge_filtered(&other.functions, &filter); } self } @@ -652,7 +652,7 @@ impl AST { #[inline(always)] pub fn retain_functions( &mut self, - filter: impl FnMut(FnNamespace, FnAccess, &str, usize) -> bool, + filter: impl Fn(FnNamespace, FnAccess, &str, usize) -> bool, ) -> &mut Self { if !self.functions.is_empty() { shared_make_mut(&mut self.functions).retain_script_functions(filter); @@ -1126,7 +1126,7 @@ pub struct FnCallExpr { /// A type that wraps a [`FLOAT`] and implements [`Hash`]. #[cfg(not(feature = "no_float"))] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, PartialOrd)] pub struct FloatWrapper(FLOAT); #[cfg(not(feature = "no_float"))] @@ -1169,14 +1169,22 @@ impl crate::stdlib::ops::DerefMut for FloatWrapper { #[cfg(not(feature = "no_float"))] impl fmt::Debug for FloatWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + fmt::Display::fmt(self, f) } } #[cfg(not(feature = "no_float"))] impl fmt::Display for FloatWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + #[cfg(feature = "no_std")] + use num_traits::Float; + + let abs = self.0.abs(); + if abs > 10000000000000.0 || abs < 0.0000000000001 { + write!(f, "{:e}", self.0) + } else { + self.0.fmt(f) + } } } @@ -1187,6 +1195,15 @@ impl From for FloatWrapper { } } +#[cfg(not(feature = "no_float"))] +impl FromStr for FloatWrapper { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + FLOAT::from_str(s).map(Into::::into) + } +} + #[cfg(not(feature = "no_float"))] impl FloatWrapper { pub const fn new(value: FLOAT) -> Self { diff --git a/src/dynamic.rs b/src/dynamic.rs index c5d9f960..c5a2743b 100644 --- a/src/dynamic.rs +++ b/src/dynamic.rs @@ -15,6 +15,9 @@ use crate::{FnPtr, ImmutableString, INT}; #[cfg(not(feature = "no_float"))] use crate::{ast::FloatWrapper, FLOAT}; +#[cfg(feature = "decimal")] +use rust_decimal::Decimal; + #[cfg(not(feature = "no_index"))] use crate::Array; @@ -25,6 +28,7 @@ use crate::Map; #[cfg(not(target_arch = "wasm32"))] use crate::stdlib::time::Instant; +use fmt::Debug; #[cfg(not(feature = "no_std"))] #[cfg(target_arch = "wasm32")] use instant::Instant; @@ -155,6 +159,8 @@ pub enum Union { Int(INT, AccessMode), #[cfg(not(feature = "no_float"))] Float(FloatWrapper, AccessMode), + #[cfg(feature = "decimal")] + Decimal(Box, AccessMode), #[cfg(not(feature = "no_index"))] Array(Box, AccessMode), #[cfg(not(feature = "no_object"))] @@ -305,6 +311,8 @@ impl Dynamic { Union::Int(_, _) => TypeId::of::(), #[cfg(not(feature = "no_float"))] Union::Float(_, _) => TypeId::of::(), + #[cfg(feature = "decimal")] + Union::Decimal(_, _) => TypeId::of::(), #[cfg(not(feature = "no_index"))] Union::Array(_, _) => TypeId::of::(), #[cfg(not(feature = "no_object"))] @@ -338,6 +346,8 @@ impl Dynamic { Union::Int(_, _) => type_name::(), #[cfg(not(feature = "no_float"))] Union::Float(_, _) => type_name::(), + #[cfg(feature = "decimal")] + Union::Decimal(_, _) => "decimal", #[cfg(not(feature = "no_index"))] Union::Array(_, _) => "array", #[cfg(not(feature = "no_object"))] @@ -408,6 +418,10 @@ pub(crate) fn map_std_type_name(name: &str) -> &str { } else if name == type_name::() { "Fn" } else { + #[cfg(feature = "decimal")] + if name == type_name::() { + return "decimal"; + } #[cfg(not(feature = "no_index"))] if name == type_name::() { return "array"; @@ -435,6 +449,8 @@ impl fmt::Display for Dynamic { Union::Int(value, _) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_float"))] Union::Float(value, _) => fmt::Display::fmt(value, f), + #[cfg(feature = "decimal")] + Union::Decimal(value, _) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_index"))] Union::Array(value, _) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] @@ -474,6 +490,8 @@ impl fmt::Debug for Dynamic { Union::Int(value, _) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_float"))] Union::Float(value, _) => fmt::Debug::fmt(value, f), + #[cfg(feature = "decimal")] + Union::Decimal(value, _) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_index"))] Union::Array(value, _) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] @@ -518,6 +536,10 @@ impl Clone for Dynamic { Union::Int(value, _) => Self(Union::Int(value, AccessMode::ReadWrite)), #[cfg(not(feature = "no_float"))] Union::Float(value, _) => Self(Union::Float(value, AccessMode::ReadWrite)), + #[cfg(feature = "decimal")] + Union::Decimal(ref value, _) => { + Self(Union::Decimal(value.clone(), AccessMode::ReadWrite)) + } #[cfg(not(feature = "no_index"))] Union::Array(ref value, _) => Self(Union::Array(value.clone(), AccessMode::ReadWrite)), #[cfg(not(feature = "no_object"))] @@ -582,6 +604,8 @@ impl Dynamic { #[cfg(not(feature = "no_float"))] Union::Float(_, access) => access, + #[cfg(feature = "decimal")] + Union::Decimal(_, access) => access, #[cfg(not(feature = "no_index"))] Union::Array(_, access) => access, #[cfg(not(feature = "no_object"))] @@ -605,6 +629,8 @@ impl Dynamic { #[cfg(not(feature = "no_float"))] Union::Float(_, access) => *access = typ, + #[cfg(feature = "decimal")] + Union::Decimal(_, access) => *access = typ, #[cfg(not(feature = "no_index"))] Union::Array(_, access) => *access = typ, #[cfg(not(feature = "no_object"))] @@ -687,6 +713,13 @@ impl Dynamic { .clone() .into(); } + #[cfg(feature = "decimal")] + if TypeId::of::() == TypeId::of::() { + return ::downcast_ref::(&value) + .unwrap() + .clone() + .into(); + } if TypeId::of::() == TypeId::of::() { return ::downcast_ref::(&value) .unwrap() @@ -845,6 +878,14 @@ impl Dynamic { }; } + #[cfg(feature = "decimal")] + if TypeId::of::() == TypeId::of::() { + return match self.0 { + Union::Decimal(value, _) => unsafe_try_cast(*value), + _ => None, + }; + } + if TypeId::of::() == TypeId::of::() { return match self.0 { Union::Bool(value, _) => unsafe_try_cast(value), @@ -1113,6 +1154,13 @@ impl Dynamic { _ => None, }; } + #[cfg(feature = "decimal")] + if TypeId::of::() == TypeId::of::() { + return match &self.0 { + Union::Decimal(value, _) => ::downcast_ref::(value.as_ref()), + _ => None, + }; + } if TypeId::of::() == TypeId::of::() { return match &self.0 { Union::Bool(value, _) => ::downcast_ref::(value), @@ -1202,6 +1250,13 @@ impl Dynamic { _ => None, }; } + #[cfg(feature = "decimal")] + if TypeId::of::() == TypeId::of::() { + return match &mut self.0 { + Union::Decimal(value, _) => ::downcast_mut::(value.as_mut()), + _ => None, + }; + } if TypeId::of::() == TypeId::of::() { return match &mut self.0 { Union::Bool(value, _) => ::downcast_mut::(value), @@ -1277,6 +1332,8 @@ impl Dynamic { } /// Cast the [`Dynamic`] as the system floating-point type [`FLOAT`] and return it. /// Returns the name of the actual type if the cast fails. + /// + /// Not available under `no_float`. #[cfg(not(feature = "no_float"))] #[inline(always)] pub fn as_float(&self) -> Result { @@ -1287,6 +1344,20 @@ impl Dynamic { _ => Err(self.type_name()), } } + /// Cast the [`Dynamic`] as a [`Decimal`] and return it. + /// Returns the name of the actual type if the cast fails. + /// + /// Available only under `decimal`. + #[cfg(feature = "decimal")] + #[inline(always)] + pub fn as_decimal(self) -> Result { + match self.0 { + Union::Decimal(n, _) => Ok(*n), + #[cfg(not(feature = "no_closure"))] + Union::Shared(_, _) => self.read_lock().map(|v| *v).ok_or_else(|| self.type_name()), + _ => Err(self.type_name()), + } + } /// Cast the [`Dynamic`] as a [`bool`] and return it. /// Returns the name of the actual type if the cast fails. #[inline(always)] @@ -1312,7 +1383,7 @@ impl Dynamic { /// Cast the [`Dynamic`] as a [`String`] and return the string slice. /// Returns the name of the actual type if the cast fails. /// - /// Cast is failing if `self` is Shared Dynamic + /// Fails if `self` is _shared_. #[inline(always)] pub fn as_str(&self) -> Result<&str, &'static str> { match &self.0 { @@ -1386,6 +1457,16 @@ impl From for Dynamic { Self(Union::Float(value, AccessMode::ReadWrite)) } } +#[cfg(feature = "decimal")] +impl From for Dynamic { + #[inline(always)] + fn from(value: Decimal) -> Self { + Self(Union::Decimal( + Box::new(value.into()), + AccessMode::ReadWrite, + )) + } +} impl From for Dynamic { #[inline(always)] fn from(value: char) -> Self { diff --git a/src/engine_api.rs b/src/engine_api.rs index ebecaa40..f45403a3 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -699,11 +699,11 @@ impl Engine { #[inline(always)] pub fn register_indexer_get_set( &mut self, - getter: impl Fn(&mut T, X) -> U + SendSync + 'static, - setter: impl Fn(&mut T, X, U) -> () + SendSync + 'static, + get_fn: impl Fn(&mut T, X) -> U + SendSync + 'static, + set_fn: impl Fn(&mut T, X, U) -> () + SendSync + 'static, ) -> &mut Self { - self.register_indexer_get(getter) - .register_indexer_set(setter) + self.register_indexer_get(get_fn) + .register_indexer_set(set_fn) } /// Register a shared [`Module`] into the global namespace of [`Engine`]. /// diff --git a/src/module/mod.rs b/src/module/mod.rs index 181d4f3b..0aba0ce7 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1334,12 +1334,12 @@ impl Module { #[inline(always)] pub fn set_indexer_get_set_fn( &mut self, - getter: impl Fn(&mut A, B) -> Result> + SendSync + 'static, - setter: impl Fn(&mut A, B, T) -> Result<(), Box> + SendSync + 'static, + get_fn: impl Fn(&mut A, B) -> Result> + SendSync + 'static, + set_fn: impl Fn(&mut A, B, T) -> Result<(), Box> + SendSync + 'static, ) -> (NonZeroU64, NonZeroU64) { ( - self.set_indexer_get_fn(getter), - self.set_indexer_set_fn(setter), + self.set_indexer_get_fn(get_fn), + self.set_indexer_set_fn(set_fn), ) } @@ -1570,7 +1570,7 @@ impl Module { pub(crate) fn merge_filtered( &mut self, other: &Self, - mut _filter: &mut impl FnMut(FnNamespace, FnAccess, bool, &str, usize) -> bool, + _filter: &impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> &mut Self { #[cfg(not(feature = "no_function"))] other.modules.iter().for_each(|(k, v)| { @@ -1625,7 +1625,7 @@ impl Module { #[inline] pub(crate) fn retain_script_functions( &mut self, - mut filter: impl FnMut(FnNamespace, FnAccess, &str, usize) -> bool, + filter: impl Fn(FnNamespace, FnAccess, &str, usize) -> bool, ) -> &mut Self { self.functions.retain( |_, diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index dbb66bb7..bfca6937 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -197,6 +197,10 @@ def_package!(crate:ArithmeticPackage:"Basic arithmetic", lib, { combine_with_exported_module!(lib, "f32", f32_functions); combine_with_exported_module!(lib, "f64", f64_functions); } + + // Decimal functions + #[cfg(feature = "decimal")] + combine_with_exported_module!(lib, "decimal", decimal_functions); }); gen_arithmetic_functions!(arith_basic => INT); @@ -257,7 +261,7 @@ mod f32_functions { } #[rhai_fn(name = "+")] pub fn plus(x: f32) -> f32 { - -x + x } pub fn abs(x: f32) -> f32 { x.abs() @@ -320,7 +324,7 @@ mod f64_functions { } #[rhai_fn(name = "+")] pub fn plus(x: f64) -> f64 { - -x + x } pub fn abs(x: f64) -> f64 { x.abs() @@ -346,3 +350,130 @@ mod f64_functions { } } } + +#[cfg(feature = "decimal")] +#[export_module] +mod decimal_functions { + use rust_decimal::{prelude::Zero, Decimal}; + + #[rhai_fn(name = "+", return_raw)] + pub fn add_dd(x: Decimal, y: Decimal) -> Result> { + if cfg!(not(feature = "unchecked")) { + x.checked_add(y) + .ok_or_else(|| make_err(format!("Addition overflow: {} + {}", x, y))) + .map(Dynamic::from) + } else { + Ok(Dynamic::from(x + y)) + } + } + #[rhai_fn(name = "+", return_raw)] + pub fn add_id(x: INT, y: Decimal) -> Result> { + add_dd(x.into(), y) + } + #[rhai_fn(name = "+", return_raw)] + pub fn add_di(x: Decimal, y: INT) -> Result> { + add_dd(x, y.into()) + } + #[rhai_fn(name = "-", return_raw)] + pub fn subtract_dd(x: Decimal, y: Decimal) -> Result> { + if cfg!(not(feature = "unchecked")) { + x.checked_sub(y) + .ok_or_else(|| make_err(format!("Subtraction overflow: {} - {}", x, y))) + .map(Dynamic::from) + } else { + Ok(Dynamic::from(x - y)) + } + } + #[rhai_fn(name = "-", return_raw)] + pub fn subtract_id(x: INT, y: Decimal) -> Result> { + subtract_dd(x.into(), y) + } + #[rhai_fn(name = "-", return_raw)] + pub fn subtract_di(x: Decimal, y: INT) -> Result> { + subtract_dd(x, y.into()) + } + #[rhai_fn(name = "*", return_raw)] + pub fn multiply_dd(x: Decimal, y: Decimal) -> Result> { + if cfg!(not(feature = "unchecked")) { + x.checked_mul(y) + .ok_or_else(|| make_err(format!("Multiplication overflow: {} * {}", x, y))) + .map(Dynamic::from) + } else { + Ok(Dynamic::from(x * y)) + } + } + #[rhai_fn(name = "*", return_raw)] + pub fn multiply_id(x: INT, y: Decimal) -> Result> { + multiply_dd(x.into(), y) + } + #[rhai_fn(name = "*", return_raw)] + pub fn multiply_di(x: Decimal, y: INT) -> Result> { + multiply_dd(x, y.into()) + } + #[rhai_fn(name = "/", return_raw)] + pub fn divide_dd(x: Decimal, y: Decimal) -> Result> { + if cfg!(not(feature = "unchecked")) { + // Detect division by zero + if y == Decimal::zero() { + Err(make_err(format!("Division by zero: {} / {}", x, y))) + } else { + x.checked_div(y) + .ok_or_else(|| make_err(format!("Division overflow: {} / {}", x, y))) + .map(Dynamic::from) + } + } else { + Ok(Dynamic::from(x / y)) + } + } + #[rhai_fn(name = "/", return_raw)] + pub fn divide_id(x: INT, y: Decimal) -> Result> { + divide_dd(x.into(), y) + } + #[rhai_fn(name = "/", return_raw)] + pub fn divide_di(x: Decimal, y: INT) -> Result> { + divide_dd(x, y.into()) + } + #[rhai_fn(name = "%", return_raw)] + pub fn modulo_dd(x: Decimal, y: Decimal) -> Result> { + if cfg!(not(feature = "unchecked")) { + x.checked_rem(y) + .ok_or_else(|| { + make_err(format!( + "Modulo division by zero or overflow: {} % {}", + x, y + )) + }) + .map(Dynamic::from) + } else { + Ok(Dynamic::from(x % y)) + } + } + #[rhai_fn(name = "%", return_raw)] + pub fn modulo_id(x: INT, y: Decimal) -> Result> { + modulo_dd(x.into(), y) + } + #[rhai_fn(name = "%", return_raw)] + pub fn modulo_di(x: Decimal, y: INT) -> Result> { + modulo_dd(x, y.into()) + } + #[rhai_fn(name = "-")] + pub fn neg(x: Decimal) -> Decimal { + -x + } + #[rhai_fn(name = "+")] + pub fn plus(x: Decimal) -> Decimal { + x + } + pub fn abs(x: Decimal) -> Decimal { + x.abs() + } + pub fn sign(x: Decimal) -> INT { + if x == Decimal::zero() { + 0 + } else if x.is_sign_negative() { + -1 + } else { + 1 + } + } +} diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index bc7f87c0..a6a82c42 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -16,6 +16,9 @@ use num_traits::float::Float; #[cfg(not(feature = "no_float"))] use crate::stdlib::format; +#[cfg(feature = "decimal")] +use rust_decimal::Decimal; + #[allow(dead_code)] #[cfg(feature = "only_i32")] pub const MAX_INT: INT = i32::MAX; @@ -23,7 +26,7 @@ pub const MAX_INT: INT = i32::MAX; #[cfg(not(feature = "only_i32"))] pub const MAX_INT: INT = i64::MAX; -macro_rules! gen_conversion_functions { +macro_rules! gen_conversion_as_functions { ($root:ident => $func_name:ident ( $($arg_type:ident),+ ) -> $result_type:ty) => { pub mod $root { $(pub mod $arg_type { use super::super::*; @@ -36,6 +39,20 @@ macro_rules! gen_conversion_functions { } } +#[cfg(feature = "decimal")] +macro_rules! gen_conversion_into_functions { + ($root:ident => $func_name:ident ( $($arg_type:ident),+ ) -> $result_type:ty) => { + pub mod $root { $(pub mod $arg_type { + use super::super::*; + + #[export_fn] + pub fn $func_name(x: $arg_type) -> $result_type { + x.into() + } + })* } + } +} + macro_rules! reg_functions { ($mod_name:ident += $root:ident :: $func_name:ident ( $($arg_type:ident),+ ) ) => { $( set_exported_fn!($mod_name, stringify!($func_name), $root::$arg_type::$func_name); @@ -76,6 +93,18 @@ def_package!(crate:BasicMathPackage:"Basic mathematic functions.", lib, { reg_functions!(lib += num_128_to_float::to_float(i128, u128)); } } + + // Decimal functions + #[cfg(feature = "decimal")] + { + combine_with_exported_module!(lib, "decimal", decimal_functions); + + reg_functions!(lib += basic_to_decimal::to_decimal(INT)); + + #[cfg(not(feature = "only_i32"))] + #[cfg(not(feature = "only_i64"))] + reg_functions!(lib += numbers_to_decimal::to_decimal(i8, u8, i16, u16, i32, u32, i64, u64)); + } }); #[export_module] @@ -267,27 +296,75 @@ mod float_functions { } } +#[cfg(feature = "decimal")] +#[export_module] +mod decimal_functions { + use rust_decimal::Decimal; + + #[rhai_fn(name = "floor", get = "floor")] + pub fn floor(x: Decimal) -> Decimal { + x.floor() + } + #[rhai_fn(name = "ceiling", get = "ceiling")] + pub fn ceiling(x: Decimal) -> Decimal { + x.ceil() + } + #[rhai_fn(name = "round", get = "round")] + pub fn round(x: Decimal) -> Decimal { + x.ceil() + } + #[rhai_fn(name = "int", get = "int")] + pub fn int(x: Decimal) -> Decimal { + x.trunc() + } + #[rhai_fn(name = "fraction", get = "fraction")] + pub fn fraction(x: Decimal) -> Decimal { + x.fract() + } + #[rhai_fn(return_raw)] + pub fn parse_decimal(s: &str) -> Result> { + s.trim() + .parse::() + .map(Into::::into) + .map_err(|err| { + EvalAltResult::ErrorArithmetic( + format!("Error parsing decimal number '{}': {}", s, err), + Position::NONE, + ) + .into() + }) + } +} + #[cfg(not(feature = "no_float"))] -gen_conversion_functions!(basic_to_float => to_float (INT) -> FLOAT); +gen_conversion_as_functions!(basic_to_float => to_float (INT) -> FLOAT); #[cfg(not(feature = "no_float"))] #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] -gen_conversion_functions!(numbers_to_float => to_float (i8, u8, i16, u16, i32, u32, i64, u64) -> FLOAT); +gen_conversion_as_functions!(numbers_to_float => to_float (i8, u8, i16, u16, i32, u32, i64, u64) -> FLOAT); #[cfg(not(feature = "no_float"))] #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] #[cfg(not(target_arch = "wasm32"))] -gen_conversion_functions!(num_128_to_float => to_float (i128, u128) -> FLOAT); +gen_conversion_as_functions!(num_128_to_float => to_float (i128, u128) -> FLOAT); -gen_conversion_functions!(basic_to_int => to_int (char) -> INT); +gen_conversion_as_functions!(basic_to_int => to_int (char) -> INT); #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] -gen_conversion_functions!(numbers_to_int => to_int (i8, u8, i16, u16, i32, u32, i64, u64) -> INT); +gen_conversion_as_functions!(numbers_to_int => to_int (i8, u8, i16, u16, i32, u32, i64, u64) -> INT); #[cfg(not(feature = "only_i32"))] #[cfg(not(feature = "only_i64"))] #[cfg(not(target_arch = "wasm32"))] -gen_conversion_functions!(num_128_to_int => to_int (i128, u128) -> INT); +gen_conversion_as_functions!(num_128_to_int => to_int (i128, u128) -> INT); + +#[cfg(feature = "decimal")] +gen_conversion_into_functions!(basic_to_decimal => to_decimal (INT) -> Decimal); + +#[cfg(feature = "decimal")] +#[cfg(not(feature = "only_i32"))] +#[cfg(not(feature = "only_i64"))] +gen_conversion_into_functions!(numbers_to_decimal => to_decimal (i8, u8, i16, u16, i32, u32, i64, u64) -> Decimal); diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index e7d55023..2f15e7b2 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -15,6 +15,9 @@ use crate::Array; #[cfg(not(feature = "no_object"))] use crate::Map; +#[cfg(feature = "decimal")] +use rust_decimal::Decimal; + const FUNC_TO_STRING: &'static str = "to_string"; const FUNC_TO_DEBUG: &'static str = "to_debug"; @@ -68,8 +71,16 @@ def_package!(crate:BasicStringPackage:"Basic string utilities, including printin #[cfg(not(feature = "no_float"))] { - reg_print_functions!(lib += print_float; f32, f64); - reg_debug_functions!(lib += debug_float; f32, f64); + reg_print_functions!(lib += print_float_64; f64); + reg_debug_functions!(lib += print_float_64; f64); + reg_print_functions!(lib += print_float_32; f32); + reg_debug_functions!(lib += print_float_32; f32); + } + + #[cfg(feature = "decimal")] + { + reg_print_functions!(lib += print_decimal; Decimal); + reg_debug_functions!(lib += debug_decimal; Decimal); } }); @@ -79,6 +90,30 @@ fn to_string(x: &mut T) -> ImmutableString { fn to_debug(x: &mut T) -> ImmutableString { format!("{:?}", x).into() } +#[cfg(not(feature = "no_float"))] +fn print_f64(x: &mut f64) -> ImmutableString { + #[cfg(feature = "no_std")] + use num_traits::Float; + + let abs = x.abs(); + if abs > 10000000000000.0 || abs < 0.0000000000001 { + format!("{:e}", x).into() + } else { + x.to_string().into() + } +} +#[cfg(not(feature = "no_float"))] +fn print_f32(x: &mut f32) -> ImmutableString { + #[cfg(feature = "no_std")] + use num_traits::Float; + + let abs = x.abs(); + if abs > 10000000000000.0 || abs < 0.0000000000001 { + format!("{:e}", x).into() + } else { + x.to_string().into() + } +} gen_functions!(print_basic => to_string(INT, bool, char, FnPtr)); gen_functions!(debug_basic => to_debug(INT, bool, Unit, char, ImmutableString)); @@ -102,10 +137,16 @@ gen_functions!(print_num_128 => to_string(i128, u128)); gen_functions!(debug_num_128 => to_debug(i128, u128)); #[cfg(not(feature = "no_float"))] -gen_functions!(print_float => to_string(f32, f64)); +gen_functions!(print_float_64 => print_f64(f64)); #[cfg(not(feature = "no_float"))] -gen_functions!(debug_float => to_debug(f32, f64)); +gen_functions!(print_float_32 => print_f32(f32)); + +#[cfg(feature = "decimal")] +gen_functions!(print_decimal => to_string(Decimal)); + +#[cfg(feature = "decimal")] +gen_functions!(debug_decimal => to_debug(Decimal)); // Register print and debug diff --git a/src/parser.rs b/src/parser.rs index eb40b99f..4cec86d3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -958,6 +958,12 @@ fn parse_primary( input.next().unwrap(); Expr::FloatConstant(x, settings.pos) } + #[cfg(feature = "decimal")] + Token::DecimalConstant(x) => { + let x = (*x).into(); + input.next().unwrap(); + Expr::DynamicConstant(Box::new(x), settings.pos) + } // { - block statement as expression Token::LeftBrace if settings.allow_stmt_expr => { diff --git a/src/serde_impl/de.rs b/src/serde_impl/de.rs index eb35ae63..f852fb89 100644 --- a/src/serde_impl/de.rs +++ b/src/serde_impl/de.rs @@ -128,12 +128,26 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { Union::Bool(_, _) => self.deserialize_bool(visitor), Union::Str(_, _) => self.deserialize_str(visitor), Union::Char(_, _) => self.deserialize_char(visitor), + #[cfg(not(feature = "only_i32"))] Union::Int(_, _) => self.deserialize_i64(visitor), #[cfg(feature = "only_i32")] Union::Int(_, _) => self.deserialize_i32(visitor), + #[cfg(not(feature = "no_float"))] + #[cfg(not(feature = "f32_float"))] Union::Float(_, _) => self.deserialize_f64(visitor), + #[cfg(not(feature = "no_float"))] + #[cfg(feature = "f32_float")] + Union::Float(_, _) => self.deserialize_f32(visitor), + + #[cfg(feature = "decimal")] + #[cfg(not(feature = "f32_float"))] + Union::Decimal(_, _) => self.deserialize_f64(visitor), + #[cfg(feature = "decimal")] + #[cfg(feature = "f32_float")] + Union::Decimal(_, _) => self.deserialize_f32(visitor), + #[cfg(not(feature = "no_index"))] Union::Array(_, _) => self.deserialize_seq(visitor), #[cfg(not(feature = "no_object"))] @@ -278,6 +292,19 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { .map_or_else(|| self.type_error(), |&x| _visitor.visit_f32(x)); #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + { + use rust_decimal::prelude::ToPrimitive; + + return self + .value + .downcast_ref::() + .and_then(|&x| x.to_f32()) + .map_or_else(|| self.type_error(), |v| _visitor.visit_f32(v)); + } + + #[cfg(feature = "no_float")] + #[cfg(not(feature = "decimal"))] return self.type_error_str("f32"); } @@ -289,6 +316,19 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> { .map_or_else(|| self.type_error(), |&x| _visitor.visit_f64(x)); #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + { + use rust_decimal::prelude::ToPrimitive; + + return self + .value + .downcast_ref::() + .and_then(|&x| x.to_f64()) + .map_or_else(|| self.type_error(), |v| _visitor.visit_f64(v)); + } + + #[cfg(feature = "no_float")] + #[cfg(not(feature = "decimal"))] return self.type_error_str("f64"); } diff --git a/src/serde_impl/deserialize.rs b/src/serde_impl/deserialize.rs index 8d998ad8..224fe5e7 100644 --- a/src/serde_impl/deserialize.rs +++ b/src/serde_impl/deserialize.rs @@ -86,6 +86,27 @@ impl<'d> Visitor<'d> for DynamicVisitor { return self.visit_f32(v as f32); } + #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + fn visit_f32(self, v: f32) -> Result { + use crate::stdlib::convert::TryFrom; + use rust_decimal::Decimal; + + Decimal::try_from(v) + .map(|v| v.into()) + .map_err(Error::custom) + } + #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + fn visit_f64(self, v: f64) -> Result { + use crate::stdlib::convert::TryFrom; + use rust_decimal::Decimal; + + Decimal::try_from(v) + .map(|v| v.into()) + .map_err(Error::custom) + } + fn visit_char(self, v: char) -> Result { self.visit_string(v.to_string()) } diff --git a/src/serde_impl/ser.rs b/src/serde_impl/ser.rs index edc272cb..167a59ef 100644 --- a/src/serde_impl/ser.rs +++ b/src/serde_impl/ser.rs @@ -214,11 +214,35 @@ impl Serializer for &mut DynamicSerializer { } fn serialize_f32(self, v: f32) -> Result> { - Ok(Dynamic::from(v)) + #[cfg(any(not(feature = "no_float"), not(feature = "decimal")))] + return Ok(Dynamic::from(v)); + + #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + { + use crate::stdlib::convert::TryFrom; + use rust_decimal::Decimal; + + Decimal::try_from(v) + .map(|v| v.into()) + .map_err(Error::custom) + } } fn serialize_f64(self, v: f64) -> Result> { - Ok(Dynamic::from(v)) + #[cfg(any(not(feature = "no_float"), not(feature = "decimal")))] + return Ok(Dynamic::from(v)); + + #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + { + use crate::stdlib::convert::TryFrom; + use rust_decimal::Decimal; + + Decimal::try_from(v) + .map(|v| v.into()) + .map_err(Error::custom) + } } fn serialize_char(self, v: char) -> Result> { diff --git a/src/serde_impl/serialize.rs b/src/serde_impl/serialize.rs index 49dbf71a..05afd31b 100644 --- a/src/serde_impl/serialize.rs +++ b/src/serde_impl/serialize.rs @@ -12,16 +12,42 @@ impl Serialize for Dynamic { Union::Bool(x, _) => ser.serialize_bool(*x), Union::Str(s, _) => ser.serialize_str(s.as_str()), Union::Char(c, _) => ser.serialize_str(&c.to_string()), + #[cfg(not(feature = "only_i32"))] Union::Int(x, _) => ser.serialize_i64(*x), #[cfg(feature = "only_i32")] Union::Int(x, _) => ser.serialize_i32(*x), + #[cfg(not(feature = "no_float"))] #[cfg(not(feature = "f32_float"))] Union::Float(x, _) => ser.serialize_f64(**x), #[cfg(not(feature = "no_float"))] #[cfg(feature = "f32_float")] Union::Float(x, _) => ser.serialize_f32(*x), + + #[cfg(feature = "decimal")] + #[cfg(not(feature = "f32_float"))] + Union::Decimal(x, _) => { + use rust_decimal::prelude::ToPrimitive; + + if let Some(v) = x.to_f64() { + ser.serialize_f64(v) + } else { + ser.serialize_str(&x.to_string()) + } + } + #[cfg(feature = "decimal")] + #[cfg(feature = "f32_float")] + Union::Decimal(x, _) => { + use rust_decimal::prelude::ToPrimitive; + + if let Some(v) = x.to_f32() { + ser.serialize_f32(v) + } else { + ser.serialize_str(&x.to_string()) + } + } + #[cfg(not(feature = "no_index"))] Union::Array(a, _) => (**a).serialize(ser), #[cfg(not(feature = "no_object"))] diff --git a/src/token.rs b/src/token.rs index b473e374..3aaa7f72 100644 --- a/src/token.rs +++ b/src/token.rs @@ -15,7 +15,10 @@ use crate::stdlib::{ use crate::{Engine, LexError, StaticVec, INT}; #[cfg(not(feature = "no_float"))] -use crate::FLOAT; +use crate::ast::FloatWrapper; + +#[cfg(feature = "decimal")] +use rust_decimal::Decimal; type LERR = LexError; @@ -153,7 +156,7 @@ impl fmt::Debug for Position { /// # Volatile Data Structure /// /// This type is volatile and may change. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Hash)] pub enum Token { /// An `INT` constant. IntegerConstant(INT), @@ -161,7 +164,12 @@ pub enum Token { /// /// Reserved under the `no_float` feature. #[cfg(not(feature = "no_float"))] - FloatConstant(FLOAT), + FloatConstant(FloatWrapper), + /// A [`Decimal`] constant. + /// + /// Requires the `decimal` feature. + #[cfg(feature = "decimal")] + DecimalConstant(Decimal), /// An identifier. Identifier(String), /// A character constant. @@ -348,6 +356,8 @@ impl Token { IntegerConstant(i) => i.to_string().into(), #[cfg(not(feature = "no_float"))] FloatConstant(f) => f.to_string().into(), + #[cfg(feature = "decimal")] + DecimalConstant(d) => d.to_string().into(), StringConstant(_) => "string".into(), CharConstant(c) => c.to_string().into(), Identifier(s) => s.clone().into(), @@ -1073,7 +1083,7 @@ fn get_next_token_inner( result.push(next_char); eat_next(stream, pos); } - #[cfg(not(feature = "no_float"))] + #[cfg(any(not(feature = "no_float"), feature = "decimal"))] '.' => { stream.get_next().unwrap(); @@ -1180,7 +1190,12 @@ fn get_next_token_inner( // If integer parsing is unnecessary, try float instead #[cfg(not(feature = "no_float"))] - let num = num.or_else(|_| FLOAT::from_str(&out).map(Token::FloatConstant)); + let num = + num.or_else(|_| FloatWrapper::from_str(&out).map(Token::FloatConstant)); + + // Then try decimal + #[cfg(feature = "decimal")] + let num = num.or_else(|_| Decimal::from_str(&out).map(Token::DecimalConstant)); return Some(( num.unwrap_or_else(|_| { diff --git a/tests/serde.rs b/tests/serde.rs index 1e1a0ad6..56b1edac 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -5,11 +5,14 @@ use rhai::{ Dynamic, Engine, EvalAltResult, ImmutableString, INT, }; use serde::{Deserialize, Serialize}; +use std::str::FromStr; #[cfg(not(feature = "no_index"))] use rhai::Array; #[cfg(not(feature = "no_object"))] use rhai::Map; +#[cfg(feature = "decimal")] +use rust_decimal::Decimal; #[test] fn test_serde_ser_primary_types() -> Result<(), Box> { @@ -25,6 +28,13 @@ fn test_serde_ser_primary_types() -> Result<(), Box> { assert!(to_dynamic(123.456_f32)?.is::()); } + #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + { + assert!(to_dynamic(123.456_f64)?.is::()); + assert!(to_dynamic(123.456_f32)?.is::()); + } + assert!(to_dynamic("hello".to_string())?.is::()); Ok(()) @@ -301,6 +311,15 @@ fn test_serde_de_primary_types() -> Result<(), Box> { assert_eq!(123.456, from_dynamic::(&Dynamic::from(123.456_f32))?); } + #[cfg(feature = "no_float")] + #[cfg(feature = "decimal")] + { + let d: Dynamic = Decimal::from_str("123.456").unwrap().into(); + + assert_eq!(123.456, from_dynamic::(&d)?); + assert_eq!(123.456, from_dynamic::(&d)?); + } + assert_eq!( "hello", from_dynamic::(&"hello".to_string().into())?