diff --git a/Cargo.toml b/Cargo.toml index 410da74..09cdd2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,17 +43,16 @@ md-5 = {version = "0.10.6", optional = true} data-encoding = { version = "2.4.0", optional = true } jsonschema = { version = "0.17.1", optional = true } +scientific = { version = "0.5.2" } regex = {version = "1.10.2", optional = true} semver = {version = "1.0.20", optional = true} wax = { version = "0.6.0", features = [], default-features = false, optional = true } url = { version = "2.5.0", optional = true } -dashu-float = { version = "0.4.1", features = ["num-traits"] } -num-traits = "0.2.17" -dashu-base = "0.4.0" uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true } + [dev-dependencies] clap = { version = "4.4.7", features = ["derive"] } colored-diff = "0.2.3" diff --git a/build.rs b/build.rs index 5cce5a3..f2d9c3f 100644 --- a/build.rs +++ b/build.rs @@ -2,10 +2,14 @@ // Licensed under the MIT License. use anyhow::Result; +use std::path::Path; fn main() -> Result<()> { // Copy hooks to appropriate location so that git will run them. - std::fs::copy("./scripts/pre-commit", "./.git/hooks/pre-commit")?; - std::fs::copy("./scripts/pre-push", "./.git/hooks/pre-push")?; + // In git worktrees, .git is a symlink and the following commands fail. + if Path::new(".git").is_dir() { + std::fs::copy("./scripts/pre-commit", "./.git/hooks/pre-commit")?; + std::fs::copy("./scripts/pre-push", "./.git/hooks/pre-push")?; + } Ok(()) } diff --git a/src/builtins/encoding.rs b/src/builtins/encoding.rs index 8ce83f0..e2326b5 100644 --- a/src/builtins/encoding.rs +++ b/src/builtins/encoding.rs @@ -3,13 +3,15 @@ use crate::ast::{Expr, Ref}; use crate::builtins; -use crate::builtins::utils::{ensure_args_count, ensure_string}; +use crate::builtins::utils::{ + ensure_args_count, ensure_object, ensure_string, ensure_string_collection, +}; use crate::lexer::Span; use crate::value::Value; use std::collections::{BTreeMap, HashMap}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { #[cfg(feature = "base64")] @@ -31,7 +33,10 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { } #[cfg(feature = "urlquery")] { + m.insert("urlquery.decode", (urlquery_decode, 1)); m.insert("urlquery.decode_object", (urlquery_decode_object, 1)); + m.insert("urlquery.encode", (urlquery_encode, 1)); + m.insert("urlquery.encode_object", (urlquery_encode_object, 1)); } m.insert("json.is_valid", (json_is_valid, 1)); m.insert("json.marshal", (json_marshal, 1)); @@ -115,11 +120,13 @@ fn base64url_decode( Err(_) => { #[cfg(feature = "base64url")] { - data_encoding::BASE64URL_NOPAD.decode(encoded_str.as_bytes())? + data_encoding::BASE64URL_NOPAD + .decode(encoded_str.as_bytes()) + .map_err(|_| anyhow!(params[0].span().error("not a valid url")))? } #[cfg(not(feature = "base64url"))] { - bail!(params[0].span().error("nor a valid url")); + bail!(params[0].span().error("not a valid url")); } } }; @@ -188,6 +195,34 @@ fn hex_encode(span: &Span, params: &[Ref], args: &[Value], _strict: bool) )) } +#[cfg(feature = "urlquery")] +fn urlquery_decode( + span: &Span, + params: &[Ref], + args: &[Value], + _strict: bool, +) -> Result { + let name = "urlquery.decode"; + ensure_args_count(span, name, params, args, 1)?; + + let string = ensure_string(name, ¶ms[0], &args[0])?; + let url_string = "https://non-existent?".to_owned() + string.as_ref(); + let url = match url::Url::parse(&url_string) { + Ok(v) => v, + Err(_) => bail!(params[0].span().error("not a valid url query")), + }; + + let mut query_str = "".to_owned(); + for (k, v) in url.query_pairs() { + query_str += &k; + if v != "" { + query_str += "="; + query_str += &v; + } + } + Ok(Value::String(query_str.into())) +} + #[cfg(feature = "urlquery")] fn urlquery_decode_object( span: &Span, @@ -195,7 +230,7 @@ fn urlquery_decode_object( args: &[Value], _strict: bool, ) -> Result { - let name = "urlquery.encode"; + let name = "urlquery.decode_object"; ensure_args_count(span, name, params, args, 1)?; let string = ensure_string(name, ¶ms[0], &args[0])?; @@ -215,7 +250,7 @@ fn urlquery_decode_object( } Ok(Value::from_map(map)) } -/* + #[cfg(feature = "urlquery")] fn urlquery_encode( span: &Span, @@ -226,17 +261,54 @@ fn urlquery_encode( let name = "urlquery.encode"; ensure_args_count(span, name, params, args, 1)?; - let string = ensure_string(name, ¶ms[0], &args[0])?; - let url_string = "https://non-existent?" + string; - let url = url::Url::parse(&url_string) - .map_err(|_| bail!(params[0].span().error("not a valid url query")))?; + let s = ensure_string(name, ¶ms[0], &args[0])?; + let mut url = match url::Url::parse("https://non-existent") { + Ok(v) => v, + Err(_) => bail!(params[0].span().error("not a valid url query")), + }; - Ok(Value::from_object( - url.query_pairs() - .map(|(k, v)| (Value::from(k.clone()), Value::from(v.clone()))) - .collect(), - )) -}*/ + url.query_pairs_mut().append_pair(&s, ""); + let query_str = url.query().unwrap_or(""); + if query_str.is_empty() { + Ok(Value::String("".into())) + } else { + Ok(Value::String(query_str[..query_str.len() - 1].into())) + } +} + +#[cfg(feature = "urlquery")] +fn urlquery_encode_object( + span: &Span, + params: &[Ref], + args: &[Value], + _strict: bool, +) -> Result { + let name = "urlquery.encode_object"; + ensure_args_count(span, name, params, args, 1)?; + + let obj = ensure_object(name, ¶ms[0], args[0].clone())?; + let mut url = match url::Url::parse("https://non-existent") { + Ok(v) => v, + Err(_) => bail!(params[0].span().error("not a valid url query")), + }; + + for (key, value) in obj.iter() { + let key = ensure_string(name, ¶ms[0], key)?; + match value { + Value::String(v) => { + url.query_pairs_mut().append_pair(key.as_ref(), v.as_ref()); + } + _ => { + let values = ensure_string_collection(name, ¶ms[0], value)?; + for v in values { + url.query_pairs_mut().append_pair(key.as_ref(), v); + } + } + } + } + + Ok(Value::String(url.query().unwrap_or("").into())) +} #[cfg(feature = "yaml")] fn yaml_is_valid( diff --git a/src/builtins/strings.rs b/src/builtins/strings.rs index cb37f14..c83ceb8 100644 --- a/src/builtins/strings.rs +++ b/src/builtins/strings.rs @@ -21,8 +21,7 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { m.insert("endswith", (endswith, 2)); m.insert("format_int", (format_int, 2)); m.insert("indexof", (indexof, 2)); - // TODO: implement this correctly. - //m.insert("indexof_n", (indexof_n, 2)); + m.insert("indexof_n", (indexof_n, 2)); m.insert("lower", (lower, 1)); m.insert("replace", (replace, 3)); m.insert("split", (split, 2)); @@ -32,7 +31,7 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { m.insert("strings.any_suffix_match", (any_suffix_match, 2)); m.insert("strings.replace_n", (replace_n, 2)); m.insert("strings.reverse", (reverse, 1)); - m.insert("strings.substring", (substring, 3)); + m.insert("substring", (substring, 3)); m.insert("trim", (trim, 2)); m.insert("trim_left", (trim_left, 2)); m.insert("trim_prefix", (trim_prefix, 2)); @@ -102,13 +101,14 @@ fn indexof(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> ensure_args_count(span, name, params, args, 2)?; let s1 = ensure_string(name, ¶ms[0], &args[0])?; let s2 = ensure_string(name, ¶ms[1], &args[1])?; - Ok(Value::from(Number::from(match s1.find(s2.as_ref()) { - Some(pos) => pos as i64, - _ => -1, - }))) + for (pos, (idx, _)) in s1.char_indices().enumerate() { + if s1[idx..].starts_with(s2.as_ref()) { + return Ok(Value::from(Number::from(pos))); + } + } + Ok(Value::from(Number::from(-1i64))) } -#[allow(dead_code)] fn indexof_n(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { let name = "indexof_n"; ensure_args_count(span, name, params, args, 2)?; @@ -116,13 +116,9 @@ fn indexof_n(span: &Span, params: &[Ref], args: &[Value], _strict: bool) - let s2 = ensure_string(name, ¶ms[1], &args[1])?; let mut positions = vec![]; - let mut idx = 0; - while idx < s1.len() { - if let Some(pos) = s1.find(s2.as_ref()) { - positions.push(Value::from(pos as u64)); - idx = pos + 1; - } else { - break; + for (pos, (idx, _)) in s1.char_indices().enumerate() { + if s1[idx..].starts_with(s2.as_ref()) { + positions.push(Value::from(Number::from(pos))); } } Ok(Value::from_array(positions)) @@ -192,6 +188,21 @@ fn to_string(v: &Value, unescape: bool) -> String { } } +enum Width { + None, + LeadingZeros(usize), + Cell(usize), + Decimals(usize), +} + +fn apply_width(w: Width, s: String) -> String { + match w { + Width::LeadingZeros(n) if n > s.len() => "0".repeat(n - s.len()) + &s, + Width::Cell(n) if n > s.len() => " ".repeat(n - s.len()) + &s, + _ => s, + } +} + fn sprintf(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { let name = "sprintf"; ensure_args_count(span, name, params, args, 2)?; @@ -203,13 +214,43 @@ fn sprintf(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> let mut chars = fmt.chars().peekable(); let args_span = params[1].span(); loop { - let verb = match chars.next() { + let (verb, width) = match chars.next() { Some('%') => match chars.next() { Some('%') => { s.push('%'); continue; } - Some(c) => c, + Some(c) if c == '.' || c.is_numeric() => { + let first_char = c; + let mut w = 0; + if c != '.' { + w = c.to_digit(10).expect("could not get digit from char"); + } + + while chars.peek().map(|c| c.is_numeric()) == Some(true) { + w = w * 10 + + chars + .next() + .expect("could not get next digit") + .to_digit(10) + .expect("could not get digit from char"); + } + let width = match first_char { + '0' => Width::LeadingZeros(w as usize), + '.' => Width::Decimals(w as usize), + _ => Width::Cell(w as usize), + }; + match chars.next() { + Some(c) => (c, width), + _ => { + let span = params[0].span(); + bail!(span.error( + "missing format verb after `%width` at end of format string" + )); + } + } + } + Some(c) => (c, Width::None), None => { let span = params[0].span(); bail!(span.error("missing format verb after `%` at end of format string")); @@ -257,7 +298,7 @@ fn sprintf(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> ('s', Value::String(sv)) => s += sv.as_ref(), ('s', v) => s += &to_string(v, false), - ('v', _) => s += format!("{arg}").as_str(), + ('v', _) => s += &to_string(arg, false), ('b', Value::Number(f)) if f.is_integer() => { let (sign, v) = get_sign_value(f); s += sign; @@ -279,43 +320,36 @@ fn sprintf(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> ('d', Value::Number(f)) if f.is_integer() => { let (sign, v) = get_sign_value(f); s += sign; - s += v.format_decimal().as_str() + s += apply_width(width, v.format_decimal()).as_str() } ('o', Value::Number(f)) if f.is_integer() => { let (sign, v) = get_sign_value(f); s += sign; - s += ("0O".to_owned() + &v.format_octal()).as_str() + s += apply_width(width, "0O".to_owned() + &v.format_octal()).as_str() } ('O', Value::Number(f)) if f.is_integer() => { let (sign, v) = get_sign_value(f); s += sign; - s += ("0o".to_owned() + &v.format_octal()).as_str() + s += apply_width(width, "0o".to_owned() + &v.format_octal()).as_str() } ('x', Value::Number(f)) if f.is_integer() => { let (sign, v) = get_sign_value(f); s += sign; - s += v.format_hex().as_str() + s += apply_width(width, v.format_hex()).as_str() } ('X', Value::Number(f)) if f.is_integer() => { let (sign, v) = get_sign_value(f); s += sign; - s += v.format_big_hex().as_str() + s += apply_width(width, v.format_big_hex()).as_str() } - ('e', Value::Number(f)) => { - s += match f.as_f64() { - Some(f) => format!("{:e}", f), - _ => bail!(span.error("cannot print large float using e format specifier")), + ('e', Value::Number(f)) => s += &f.format_scientific(), + ('E', Value::Number(f)) => s += &f.format_scientific().replace('e', "E"), + ('f' | 'F', Value::Number(f)) => { + s += &match width { + Width::Decimals(d) => f.format_decimal_with_width(d as u32), + _ => apply_width(width, f.format_decimal()), } - .as_str() } - ('E', Value::Number(f)) => { - s += match f.as_f64() { - Some(f) => format!("{:E}", f), - _ => bail!(span.error("cannot print large float using E format specifier")), - } - .as_str() - } - ('f' | 'F', Value::Number(f)) => s += f.format_decimal().as_str(), ('g', Value::Number(f)) => { let (sign, v) = get_sign_value(f); let v = match v.as_f64() { @@ -349,8 +383,7 @@ fn sprintf(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> } } (_, Value::Number(_)) => { - // TODO: binary for floating point. - bail!(args_span.error("floating-point number specified for format verb {verb}.")); + bail!(args_span.error(&format!("number specified for format verb {verb}."))); } ('+', _) if chars.next() == Some('v') => { @@ -382,31 +415,45 @@ fn any_prefix_match( span: &Span, params: &[Ref], args: &[Value], - _strict: bool, + strict: bool, ) -> Result { let name = "strings.any_prefix_match"; ensure_args_count(span, name, params, args, 2)?; let search = match &args[0] { Value::String(s) => vec![s.as_ref()], - Value::Array(_) | Value::Set(_) => ensure_string_collection(name, ¶ms[0], &args[0])?, - _ => { + Value::Array(_) | Value::Set(_) => { + match ensure_string_collection(name, ¶ms[0], &args[0]) { + Ok(c) => c, + Err(e) if strict => return Err(e), + _ => return Ok(Value::Undefined), + } + } + _ if strict => { let span = params[0].span(); bail!(span.error( format!("`{name}` expects string/array[string]/set[string] argument.").as_str() )); } + _ => return Ok(Value::Undefined), }; let base = match &args[1] { Value::String(s) => vec![s.as_ref()], - Value::Array(_) | Value::Set(_) => ensure_string_collection(name, ¶ms[1], &args[1])?, - _ => { + Value::Array(_) | Value::Set(_) => { + match ensure_string_collection(name, ¶ms[1], &args[1]) { + Ok(c) => c, + Err(e) if strict => return Err(e), + _ => return Ok(Value::Undefined), + } + } + _ if strict => { let span = params[0].span(); bail!(span.error( format!("`{name}` expects string/array[string]/set[string] argument.").as_str() )); } + _ => return Ok(Value::Undefined), }; Ok(Value::Bool( @@ -418,31 +465,45 @@ fn any_suffix_match( span: &Span, params: &[Ref], args: &[Value], - _strict: bool, + strict: bool, ) -> Result { let name = "strings.any_suffix_match"; ensure_args_count(span, name, params, args, 2)?; let search = match &args[0] { Value::String(s) => vec![s.as_ref()], - Value::Array(_) | Value::Set(_) => ensure_string_collection(name, ¶ms[0], &args[0])?, - _ => { + Value::Array(_) | Value::Set(_) => { + match ensure_string_collection(name, ¶ms[0], &args[0]) { + Ok(c) => c, + Err(e) if strict => return Err(e), + _ => return Ok(Value::Undefined), + } + } + _ if strict => { let span = params[0].span(); bail!(span.error( format!("`{name}` expects string/array[string]/set[string] argument.").as_str() )); } + _ => return Ok(Value::Undefined), }; let base = match &args[1] { Value::String(s) => vec![s.as_ref()], - Value::Array(_) | Value::Set(_) => ensure_string_collection(name, ¶ms[1], &args[1])?, - _ => { + Value::Array(_) | Value::Set(_) => { + match ensure_string_collection(name, ¶ms[1], &args[1]) { + Ok(c) => c, + Err(e) if strict => return Err(e), + _ => return Ok(Value::Undefined), + } + } + _ if strict => { let span = params[0].span(); bail!(span.error( format!("`{name}` expects string/array[string]/set[string] argument.").as_str() )); } + _ => return Ok(Value::Undefined), }; Ok(Value::Bool( @@ -488,28 +549,24 @@ fn reverse(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Ok(Value::String(s.chars().rev().collect::().into())) } -fn substring(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { +fn substring(span: &Span, params: &[Ref], args: &[Value], strict: bool) -> Result { let name = "substring"; ensure_args_count(span, name, params, args, 3)?; let s = ensure_string(name, ¶ms[0], &args[0])?; let offset = ensure_numeric(name, ¶ms[1], &args[1])?; let length = ensure_numeric(name, ¶ms[2], &args[2])?; - // TODO: distinguish between 20.0 and 20 - // Also: behavior of - // x = substring("hello", 20 + 0.0, 25) - match (offset.as_u64(), length.as_u64()) { - (Some(offset), Some(length)) => { - let offset = offset as usize; - let length = length as usize; - - if offset > s.len() || length <= offset { - return Ok(Value::String("".into())); - } - - Ok(Value::String(s[offset..offset + length].into())) + match (offset.as_i64(), length.as_i64()) { + (Some(offset), _) if offset < 0 && strict => { + bail!(params[1].span().error("negative offset")) } - _ => Ok(Value::Undefined), + (Some(offset), _) if offset < 0 => Ok(Value::Undefined), + (Some(offset), Some(length)) => { + let start = s.chars().skip(offset as usize); + let length = if length < 0 { s.len() } else { length as usize }; + Ok(Value::String(start.take(length).collect::().into())) + } + _ => Ok(Value::String("".into())), } } diff --git a/src/builtins/units.rs b/src/builtins/units.rs index 6598c65..d3ea4b6 100644 --- a/src/builtins/units.rs +++ b/src/builtins/units.rs @@ -102,10 +102,10 @@ fn parse(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re }; if let Some(e) = ten_exp(suffix) { - n.mul_assign(&Number::ten_pow(e))?; + n.mul_assign(&Number::ten_pow(e)?)?; Ok(Value::from(n)) } else if let Some(e) = two_exp(suffix) { - n.mul_assign(&Number::two_pow(e))?; + n.mul_assign(&Number::two_pow(e)?)?; Ok(Value::from(n)) } else { return Ok(Value::Undefined); @@ -182,10 +182,10 @@ fn parse_bytes(span: &Span, params: &[Ref], args: &[Value], strict: bool) }; if let Some(e) = twob_exp(suffix) { - n.mul_assign(&Number::two_pow(e))?; + n.mul_assign(&Number::two_pow(e)?)?; Ok(Value::from(n.round())) } else if let Some(e) = tenb_exp(suffix) { - n.mul_assign(&Number::ten_pow(e))?; + n.mul_assign(&Number::ten_pow(e)?)?; Ok(Value::from(n.round())) } else { Ok(Value::Undefined) diff --git a/src/number.rs b/src/number.rs index c2977d7..237e72f 100644 --- a/src/number.rs +++ b/src/number.rs @@ -3,21 +3,18 @@ use core::fmt::{Debug, Formatter}; use std::cmp::{Ord, Ordering}; -use std::ops::{AddAssign, Div, MulAssign, SubAssign}; use std::rc::Rc; use std::str::FromStr; -use anyhow::{bail, Result}; -use dashu_float; -use num_traits::cast::ToPrimitive; +use anyhow::{anyhow, bail, Result}; use serde::ser::Serializer; use serde::Serialize; pub type BigInt = i128; -type BigFloat = dashu_float::DBig; -const PRECISION: usize = 100; +type BigFloat = scientific::Scientific; +const PRECISION: scientific::Precision = scientific::Precision::Digits(100); #[derive(Clone, Debug, PartialEq)] pub struct BigDecimal { @@ -45,28 +42,14 @@ impl From for BigDecimal { impl From for BigDecimal { fn from(value: i128) -> Self { BigDecimal { - d: Into::::into(value) - .with_precision(PRECISION) - .value(), + d: Into::::into(value), } } } -impl Serialize for BigDecimal { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let s = self.d.to_string(); - let v = serde_json::Number::from_str(&s) - .map_err(|_| serde::ser::Error::custom("could not serialize big number"))?; - v.serialize(serializer) - } -} - impl BigDecimal { fn is_integer(&self) -> bool { - self.d.floor() == self.d + self.d.decimals() <= 0 } } @@ -90,22 +73,11 @@ impl Serialize for Number { S: Serializer, { match self { - Big(b) => { - if let Some(n) = self.as_u64() { - n.serialize(serializer) - } else if let Some(n) = self.as_i64() { - n.serialize(serializer) - } else { - if let Some(f) = self.as_f64() { - if b.d.digits() <= 15 { - return f.serialize(serializer); - } - } - let s = b.d.to_string(); - let v = serde_json::Number::from_str(&s) - .map_err(|_| serde::ser::Error::custom("could not serialize big number"))?; - v.serialize(serializer) - } + Big(_) => { + let s = self.format_decimal(); + let v = serde_json::Number::from_str(&s) + .map_err(|_| serde::ser::Error::custom("could not serialize big number"))?; + v.serialize(serializer) } } } @@ -115,7 +87,7 @@ use Number::*; impl From for Number { fn from(n: BigFloat) -> Self { - Self::Big(BigDecimal::from(n.with_precision(PRECISION).value()).into()) + Self::Big(BigDecimal::from(n).into()) } } @@ -162,21 +134,33 @@ impl From for Number { impl Number { pub fn as_u64(&self) -> Option { match self { - Big(b) if b.is_integer() => b.d.to_u64(), + Big(b) if b.is_integer() => match u64::try_from(&b.d) { + Ok(v) => Some(v), + _ => None, + }, _ => None, } } pub fn as_i64(&self) -> Option { match self { - Big(b) if b.is_integer() => b.d.to_i64(), + Big(b) if b.is_integer() => match i64::try_from(&b.d) { + Ok(v) => Some(v), + _ => None, + }, _ => None, } } pub fn as_f64(&self) -> Option { match self { - Big(b) => Some(b.d.to_binary().value().to_f64().value()), + Big(b) => { + let f = f64::from(&b.d); + match BigFloat::try_from(f) { + Ok(bf) if bf == b.d => Some(f), + _ => None, + } + } } } @@ -234,61 +218,56 @@ impl PartialOrd for Number { impl Number { pub fn add_assign(&mut self, rhs: &Self) -> Result<()> { - match (self, rhs) { - (Big(ref mut a), Big(b)) => { - Rc::make_mut(a).d.add_assign(&b.d); - } - } + *self = self.add(rhs)?; Ok(()) } pub fn add(&self, rhs: &Self) -> Result { - let mut c = self.clone(); - c.add_assign(rhs)?; - Ok(c) + match (self, rhs) { + (Big(a), Big(b)) => Ok(Big(BigDecimal::from(&a.d + &b.d).into())), + } } pub fn sub_assign(&mut self, rhs: &Self) -> Result<()> { - match (self, rhs) { - (Big(ref mut a), Big(b)) => { - Rc::make_mut(a).d.sub_assign(&b.d); - } - } + *self = self.sub(rhs)?; Ok(()) } pub fn sub(&self, rhs: &Self) -> Result { - let mut c = self.clone(); - c.sub_assign(rhs)?; - Ok(c) + match (self, rhs) { + (Big(a), Big(b)) => Ok(Big(BigDecimal::from(&a.d - &b.d).into())), + } } pub fn mul_assign(&mut self, rhs: &Self) -> Result<()> { - match (self, rhs) { - (Big(ref mut a), Big(b)) => { - Rc::make_mut(a).d.mul_assign(&b.d); - } - } + *self = self.mul(rhs)?; Ok(()) } pub fn mul(&self, rhs: &Self) -> Result { - let mut c = self.clone(); - c.mul_assign(rhs)?; - Ok(c) + match (self, rhs) { + (Big(a), Big(b)) => Ok(Big(BigDecimal::from(&a.d * &b.d).into())), + } } pub fn divide(self, rhs: &Self) -> Result { - Ok(match (self, rhs) { - (Big(a), Big(b)) => a.d.clone().div(&b.d).into(), - }) + match (self, rhs) { + (Big(a), Big(b)) => { + let c = + a.d.div_truncate(&b.d, PRECISION) + .map_err(|e| anyhow!("{e}"))?; + Ok(Big(BigDecimal::from(c).into())) + } + } } pub fn modulo(self, rhs: &Self) -> Result { - use dashu_base::RemEuclid; - Ok(match (self, rhs) { - (Big(a), Big(b)) => a.d.clone().rem_euclid(&b.d).into(), - }) + match (self, rhs) { + (Big(a), Big(b)) => { + let (_, c) = a.d.div_rem(&b.d).map_err(|e| anyhow!("{e}"))?; + Ok(Big(BigDecimal::from(c).into())) + } + } } pub fn is_integer(&self) -> bool { @@ -299,15 +278,15 @@ impl Number { pub fn is_positive(&self) -> bool { match self { - Big(b) => b.d.sign() == dashu_base::Sign::Positive, + Big(b) => b.d.is_sign_positive(), } } fn ensure_integers(a: &Number, b: &Number) -> Option<(BigInt, BigInt)> { match (a, b) { (Big(a), Big(b)) if a.is_integer() && b.is_integer() => { - match (a.d.to_i128(), b.d.to_i128()) { - (Some(a), Some(b)) => Some((a, b)), + match (BigInt::try_from(&a.d), BigInt::try_from(&b.d)) { + (Ok(a), Ok(b)) => Some((a, b)), _ => None, } } @@ -317,7 +296,10 @@ impl Number { fn ensure_integer(&self) -> Option { match self { - Big(a) if a.is_integer() => a.d.to_i128(), + Big(a) if a.is_integer() => match BigInt::try_from(&a.d) { + Ok(v) => Some(v), + _ => None, + }, _ => None, } } @@ -362,7 +344,6 @@ impl Number { } pub fn abs(&self) -> Number { - use dashu_base::Abs; match self { Big(b) => b.d.clone().abs().into(), } @@ -370,38 +351,48 @@ impl Number { pub fn floor(&self) -> Number { match self { - Big(b) => b.d.floor().into(), + Big(b) => Big(BigDecimal::from(b.d.round( + scientific::Precision::Decimals(0), + scientific::Rounding::RoundDown, + )) + .into()), } } pub fn ceil(&self) -> Number { match self { - Big(b) => b.d.ceil().into(), + Big(b) => Big(BigDecimal::from(b.d.round( + scientific::Precision::Decimals(0), + scientific::Rounding::RoundUp, + )) + .into()), } } pub fn round(&self) -> Number { match self { - Big(b) => b.d.round().into(), + Big(b) => Big(BigDecimal::from(b.d.round( + scientific::Precision::Decimals(0), + scientific::Rounding::RoundHalfAwayFromZero, + )) + .into()), } } - pub fn two_pow(e: i32) -> Number { - use num_traits::Pow; - BigFloat::from(2) - .with_precision(80) - .value() - .pow(&BigFloat::from(e)) - .into() + pub fn two_pow(e: i32) -> Result { + if e >= 0 { + Ok(BigFloat::from(2).powi(e as usize).into()) + } else { + Number::from(1u64).divide(&BigFloat::from(2).powi(-e as usize).into()) + } } - pub fn ten_pow(e: i32) -> Number { - use num_traits::Pow; - BigFloat::from(10) - .with_precision(80) - .value() - .pow(&BigFloat::from(e)) - .into() + pub fn ten_pow(e: i32) -> Result { + if e >= 0 { + Ok(BigFloat::from(10).powi(e as usize).into()) + } else { + Number::from(1u64).divide(&BigFloat::from(10).powi(-e as usize).into()) + } } pub fn format_bin(&self) -> String { @@ -416,10 +407,58 @@ impl Number { .unwrap_or("".to_string()) } + pub fn format_scientific(&self) -> String { + match self { + Big(b) => format!("{}", b.d), + } + } + pub fn format_decimal(&self) -> String { - self.ensure_integer() - .map(|a| format!("{}", a)) - .unwrap_or("".to_string()) + if let Some(u) = self.as_u64() { + u.to_string() + } else if let Some(i) = self.as_i64() { + i.to_string() + } else if let Some(f) = self.as_f64() { + f.to_string() + } else { + let s = match self { + Big(b) => format!("{}", b.d), + }; + + // Remove trailing e0 + if s.ends_with("e0") { + return s[..s.len() - 2].to_string(); + } + + // Avoid e notation if full mantissa is written out. + let parts: Vec<&str> = s.split('e').collect(); + match self { + Big(b) => { + if b.d.is_sign_positive() { + if parts[0].len() == b.d.exponent1() as usize + 2 { + return parts[0].replace('.', ""); + } + } else if parts[0].len() == b.d.exponent1() as usize + 3 { + return parts[0].replace('.', ""); + } + } + } + + s + } + } + + pub fn format_decimal_with_width(&self, d: u32) -> String { + match self { + Big(b) => { + let n = Big(BigDecimal::from(b.d.round( + scientific::Precision::Decimals(d as isize), + scientific::Rounding::RoundHalfAwayFromZero, + )) + .into()); + n.format_decimal() + } + } } pub fn format_hex(&self) -> String { diff --git a/tests/opa.passing b/tests/opa.passing index 03e1930..7b11c7e 100644 --- a/tests/opa.passing +++ b/tests/opa.passing @@ -88,6 +88,7 @@ semvercompare semverisvalid sets sprintf +strings subset toarray topdowndynamicdispatch @@ -104,6 +105,7 @@ typenamebuiltin undos union units +urlbuiltins uuid varreferences virtualdocs \ No newline at end of file