From b80509a8be4d77149388311a486c754cb4bb1a76 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 5 Jul 2023 02:45:20 -0400 Subject: [PATCH 01/21] Begin v0.4.1 development --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8ae8805..bb51afb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.0" +version = "0.4.1+dev" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal" From be77b62b7848f5281c77f6bd0e85e0b26c263f7c Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Thu, 6 Jul 2023 21:52:39 -0400 Subject: [PATCH 02/21] Make cargo:check-{1.54,1.70} manual pipelines --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9e6e081..23b65a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -182,6 +182,7 @@ cargo:test-1.43: cargo:check-1.54: stage: check image: "akubera/rust-kcov:1.54.0-bullseye" + when: manual variables: RUST_CACHE_KEY: "1.54" <<: *cargo-check-script @@ -208,6 +209,7 @@ cargo:test-1.54: cargo:check-1.70: stage: check image: "akubera/rust-grcov:1.70.0-bullseye" + when: manual variables: RUST_CACHE_KEY: "1.70" <<: *cargo-check-script From 5977e30d18fff694661da75f9ccf174fe1711563 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 7 Jul 2023 01:22:50 -0400 Subject: [PATCH 03/21] Restructure gitlab-ci file --- .gitlab-ci.yml | 194 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 126 insertions(+), 68 deletions(-) mode change 100644 => 100755 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml old mode 100644 new mode 100755 index 23b65a3..c4d2c80 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,31 +1,98 @@ +# Gitlab CI Config +# ================ +# +# Defines jobs for checking/building/testing various +# configurations of the BigDecimal crate. +# +# Always tests against stable and MinimumSupportedRustVersion (1.43) +# Other random versions are available, requiring manual action to run. +# +# Most jobs are run upon custom rust containers I try to keep updated: +# https://gitlab.com/akubera/containers/-/blob/master/akubera-rust/Dockerfile.in +# https://hub.docker.com/r/akubera/rust/tags +# +# A coverage report is done via one of my rust-kcov or rust-grcov containers: +# https://gitlab.com/akubera/containers/-/blob/master/rust-kcov/Dockerfile.in +# https://gitlab.com/akubera/containers/-/blob/master/rust-grcov/Dockerfile.in +# +# A cargo:benchmark job is available, which saves a criterion report to gitlab, +# manualy activated and of dubious reliability without control of target machine. +# + +#----------------------------# +# Yaml "Template" Objects # +#----------------------------# + +.script: + cargo-check: &script-cargo-check + before_script: + - rustc --version && cargo --version + script: + - cargo check --tests + +.script-cargo-build: &script-cargo-build + before_script: + - rustc --version && cargo --version + script: + - cargo build --tests + +.script-cargo-test: &script-cargo-test + before_script: + - rustc --version && cargo --version + script: + - cargo test --verbose + + +.cache-cargo-registry: &cache-cargo-registry + key: cargo-registry + policy: pull + paths: + - .cargo/registry/index + - .cargo/registry/cache + - target/debug/deps + +.cache-cargo-build: &cache-cargo-build + key: rustbuild-${RUST_CACHE_KEY} + policy: pull-push + paths: + - target/debug/deps + - target/debug/build + +.cache-cargo-semver-check: &cache-cargo-semver-check + key: cargo-semver-checks + policy: pull-push + paths: + - target/semver-checks/cache + +.rules.never-without-branch: + rules: &rules-never-without-branch + - if: '$CI_COMMIT_BRANCH == null' + when: never + +.rules:always-master-otherwise-manual: + rules: &rules-always-master-otherwise-manual + - if: '$CI_COMMIT_BRANCH == "master"' + when: always + - if: '$CI_COMMIT_BRANCH == null' + when: never + - when: manual + + +#---------------------# +# Global Settings # +#---------------------# cache: - - key: cargo-registry - policy: pull - paths: - - .cargo/registry/index - - .cargo/registry/cache - - target/debug/deps - # each job manually specifies its build cache - no way to automate on version - - key: rustbuild-${RUST_CACHE_KEY} - policy: pull-push - paths: - - target/debug/deps - - target/debug/build + - <<: *cache-cargo-registry + - <<: *cache-cargo-build variables: - # store cargo registry in the project directory + # store cargo registry in the project directory (for caching) CARGO_HOME: ${CI_PROJECT_DIR}/.cargo # this fixes an error when updating cargo registry CARGO_NET_GIT_FETCH_WITH_CLI: 'true' - # default arguments to pass to cargo test - CARGO_BUILD_ARGS: "" - - # default arguments to pass to cargo test - CARGO_TEST_ARGS: "--verbose" - stages: - check @@ -34,42 +101,21 @@ stages: - deploy -.cargo-check-script: &cargo-check-script - script: - - rustc --version && cargo --version - - cargo check - -.cargo-build-script: &cargo-build-script - script: - - rustc --version && cargo --version - - printenv CARGO_BUILD_ARGS - - cargo build $CARGO_BUILD_ARGS - -.cargo-test-script: &cargo-test-script - script: - - rustc --version && cargo --version - - printenv CARGO_TEST_ARGS - - cargo test $CARGO_TEST_ARGS +#---------------------# +# CI Jobs # +#---------------------# cargo:check: stage: check image: akubera/rust:stable cache: - - key: cargo-registry - policy: pull-push - paths: - - .cargo/registry/index - - .cargo/registry/cache - - target/debug/deps - - key: rustbuild-${RUST_CACHE_KEY} + - <<: *cache-cargo-registry policy: pull-push - paths: - - target/debug/deps - - target/debug/build + - <<: *cache-cargo-build variables: RUST_CACHE_KEY: "stable" - <<: *cargo-check-script + <<: *script-cargo-check cargo:clippy: stage: check @@ -79,6 +125,8 @@ cargo:clippy: allow_failure: true variables: RUST_CACHE_KEY: "stable" + before_script: + - rustc --version && cargo --version && cargo clippy --version script: - cargo clippy -- -Dclippy::{dbg_macro,todo} @@ -88,8 +136,11 @@ cargo:semver-checks: needs: - cargo:check allow_failure: true - variables: - RUST_CACHE_KEY: "stable" + cache: + - <<: *cache-cargo-registry + - <<: *cache-cargo-semver-check + before_script: + - rustc --version && cargo --version && cargo semver-checks --version script: - cargo semver-checks @@ -101,7 +152,7 @@ cargo:build-stable: - cargo:check variables: RUST_CACHE_KEY: "stable" - <<: *cargo-build-script + <<: *script-cargo-build cargo:test-stable: stage: test @@ -110,7 +161,8 @@ cargo:test-stable: - "cargo:build-stable" variables: RUST_CACHE_KEY: "stable" - <<: *cargo-test-script + <<: *script-cargo-test + cargo:build:no-std: stage: build @@ -119,8 +171,9 @@ cargo:build:no-std: - cargo:check variables: RUST_CACHE_KEY: "stable+no_std" - CARGO_BUILD_ARGS: "--no-default-features --lib" - <<: *cargo-build-script + <<: *script-cargo-build + script: + - cargo build --no-default-features --lib cargo:test:no-std: stage: test @@ -129,8 +182,9 @@ cargo:test:no-std: - "cargo:build:no-std" variables: RUST_CACHE_KEY: "stable+no_std" - CARGO_TEST_ARGS: "--no-default-features --lib" - <<: *cargo-test-script + <<: *script-cargo-test + script: + - cargo test --no-default-features --lib cargo:build-nightly: @@ -139,7 +193,7 @@ cargo:build-nightly: allow_failure: true variables: RUST_CACHE_KEY: "nightly" - <<: *cargo-build-script + <<: *script-cargo-build cargo:test-nightly: @@ -150,7 +204,7 @@ cargo:test-nightly: allow_failure: true variables: RUST_CACHE_KEY: "nightly" - <<: *cargo-test-script + <<: *script-cargo-test cargo:check-1.43: @@ -158,7 +212,7 @@ cargo:check-1.43: image: "akubera/rust-kcov:1.43.1-buster" variables: RUST_CACHE_KEY: "1.43" - <<: *cargo-check-script + <<: *script-cargo-check cargo:build-1.43: stage: build @@ -167,7 +221,7 @@ cargo:build-1.43: - "cargo:check-1.43" variables: RUST_CACHE_KEY: "1.43" - <<: *cargo-build-script + <<: *script-cargo-build cargo:test-1.43: stage: test @@ -176,16 +230,17 @@ cargo:test-1.43: image: "akubera/rust-kcov:1.43.1-buster" variables: RUST_CACHE_KEY: "1.43" - <<: *cargo-test-script + <<: *script-cargo-test cargo:check-1.54: stage: check image: "akubera/rust-kcov:1.54.0-bullseye" - when: manual + rules: + *rules-always-master-otherwise-manual variables: RUST_CACHE_KEY: "1.54" - <<: *cargo-check-script + <<: *script-cargo-check cargo:build-1.54: stage: build @@ -194,7 +249,7 @@ cargo:build-1.54: - "cargo:check-1.54" variables: RUST_CACHE_KEY: "1.54" - <<: *cargo-build-script + <<: *script-cargo-build cargo:test-1.54: stage: test @@ -203,16 +258,17 @@ cargo:test-1.54: image: "akubera/rust-kcov:1.54.0-bullseye" variables: RUST_CACHE_KEY: "1.54" - <<: *cargo-test-script + <<: *script-cargo-test cargo:check-1.70: stage: check image: "akubera/rust-grcov:1.70.0-bullseye" - when: manual + rules: + *rules-always-master-otherwise-manual variables: RUST_CACHE_KEY: "1.70" - <<: *cargo-check-script + <<: *script-cargo-check cargo:build-1.70: stage: build @@ -221,7 +277,7 @@ cargo:build-1.70: - "cargo:check-1.70" variables: RUST_CACHE_KEY: "1.70" - <<: *cargo-build-script + <<: *script-cargo-build cargo:test-1.70: stage: test @@ -230,7 +286,7 @@ cargo:test-1.70: image: "akubera/rust-grcov:1.70.0-bullseye" variables: RUST_CACHE_KEY: "1.70" - <<: *cargo-test-script + <<: *script-cargo-test coverage-test: @@ -248,8 +304,9 @@ coverage-test: CARGO_INCREMENTAL: "0" coverage: '/Code Coverage: \d+\.\d+/' - script: + before_script: - rustc --version && cargo --version + script: - cargo test - ls -l target/coverage - grcov target/coverage --binary-path target/debug -s . --keep-only 'src/*' -tcobertura -o cobertura.xml @@ -272,6 +329,7 @@ cargo:benchmark: image: "akubera/bigdecimal-benchmark-base:1.70.0-bullseye" when: manual allow_failure: true + # No cache as benchmark-image stores registry in different location cache: [] variables: RUST_CACHE_KEY: "1.70" From ed7279de0009c714446e20a4bdf90cc986efd7b9 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Fri, 7 Jul 2023 01:23:31 -0400 Subject: [PATCH 04/21] Ignore archives, backups, and Python files in gitignore --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 3be4a6a..62216a9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,12 @@ target benches/test-data/ builds/ + +*.tgz +*.tar +*.bak +*.backup + +*venv*/ +.python-version +*.ipynb From c274173b3db32be6d827976a40c9526c3785e056 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sun, 9 Jul 2023 19:36:19 -0400 Subject: [PATCH 05/21] Fix build.rs so rebuild is not always triggered by writing default-precision.rs file Co-authored-by: Andrew Kubera --- build.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/build.rs b/build.rs index 8bc8be9..b520ea6 100644 --- a/build.rs +++ b/build.rs @@ -2,8 +2,6 @@ use std::env; -use std::fs::File; -use std::io::Write; use std::path::PathBuf; fn main() -> std::io::Result<()> { @@ -28,8 +26,17 @@ fn write_default_precision(outdir_path: &PathBuf, filename: &str) -> std::io::Re let default_precision_rs_path = outdir_path.join(filename); - let mut default_precision_rs = File::create(&default_precision_rs_path).expect("Could not create default_precision.rs"); - write!(default_precision_rs, "const DEFAULT_PRECISION: u64 = {};", default_prec)?; + let default_precision = format!("const DEFAULT_PRECISION: u64 = {};", default_prec); + + // Rewriting the file if it already exists with the same contents + // would force a rebuild. + match std::fs::read_to_string(&default_precision_rs_path) { + Ok(existing_contents) if existing_contents == default_precision => {}, + _ => { + std::fs::write(&default_precision_rs_path, default_precision) + .expect("Could not write big decimal default-precision file"); + } + }; println!("cargo:rerun-if-changed={}", default_precision_rs_path.display()); println!("cargo:rerun-if-env-changed={}", "RUST_BIGDECIMAL_DEFAULT_PRECISION"); From 4fa34f33ebea971521d26233a2ddbd4de45a0579 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 11 Jul 2023 02:04:34 -0400 Subject: [PATCH 06/21] Move implementation of From<{int}> and From<&{int}> to impl_convert.rs --- src/impl_convert.rs | 37 ++++++++++++++++++++++++++++ src/lib.rs | 59 +++------------------------------------------ 2 files changed, 40 insertions(+), 56 deletions(-) create mode 100644 src/impl_convert.rs diff --git a/src/impl_convert.rs b/src/impl_convert.rs new file mode 100644 index 0000000..35185e8 --- /dev/null +++ b/src/impl_convert.rs @@ -0,0 +1,37 @@ +//! Code for implementing From/To BigDecimals + +use super::BigDecimal; + + +macro_rules! impl_from_int_primitive { + ($t:ty) => { + impl From<$t> for BigDecimal { + fn from(n: $t) -> Self { + BigDecimal { + int_val: n.into(), + scale: 0, + } + } + } + + impl From<&$t> for BigDecimal { + fn from(n: &$t) -> Self { + BigDecimal { + int_val: (*n).into(), + scale: 0, + } + } + } + }; +} + +impl_from_int_primitive!(u8); +impl_from_int_primitive!(u16); +impl_from_int_primitive!(u32); +impl_from_int_primitive!(u64); +impl_from_int_primitive!(u128); +impl_from_int_primitive!(i8); +impl_from_int_primitive!(i16); +impl_from_int_primitive!(i32); +impl_from_int_primitive!(i64); +impl_from_int_primitive!(i128); diff --git a/src/lib.rs b/src/lib.rs index ff06a97..d77d851 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,6 +90,9 @@ mod macros; #[cfg(test)] extern crate paste; +// From, To, TryFrom impls +mod impl_convert; + mod parsing; pub mod rounding; pub use rounding::RoundingMode; @@ -2096,43 +2099,6 @@ impl ToPrimitive for BigDecimal { } } -impl From for BigDecimal { - #[inline] - fn from(n: i64) -> Self { - BigDecimal { - int_val: BigInt::from(n), - scale: 0, - } - } -} - -impl From for BigDecimal { - #[inline] - fn from(n: u64) -> Self { - BigDecimal { - int_val: BigInt::from(n), - scale: 0, - } - } -} - -impl From for BigDecimal { - fn from(n: i128) -> Self { - BigDecimal { - int_val: BigInt::from(n), - scale: 0, - } - } -} - -impl From for BigDecimal { - fn from(n: u128) -> Self { - BigDecimal { - int_val: BigInt::from(n), - scale: 0, - } - } -} impl From<(BigInt, i64)> for BigDecimal { #[inline] @@ -2154,25 +2120,6 @@ impl From for BigDecimal { } } -macro_rules! impl_from_type { - ($FromType:ty, $AsType:ty) => { - impl From<$FromType> for BigDecimal { - #[inline] - #[allow(clippy::cast_lossless)] - fn from(n: $FromType) -> Self { - BigDecimal::from(n as $AsType) - } - } - }; -} - -impl_from_type!(u8, u64); -impl_from_type!(u16, u64); -impl_from_type!(u32, u64); - -impl_from_type!(i8, i64); -impl_from_type!(i16, i64); -impl_from_type!(i32, i64); impl TryFrom for BigDecimal { type Error = ParseBigDecimalError; From b405d06cf05870141f7fb092f99b6a0d82c4faca Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 11 Jul 2023 02:07:06 -0400 Subject: [PATCH 07/21] Move impls of TryFrom into impl_convert --- src/impl_convert.rs | 22 +++++++++++++++++++++- src/lib.rs | 18 ------------------ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/impl_convert.rs b/src/impl_convert.rs index 35185e8..ff3f955 100644 --- a/src/impl_convert.rs +++ b/src/impl_convert.rs @@ -1,6 +1,7 @@ //! Code for implementing From/To BigDecimals -use super::BigDecimal; +use crate::BigDecimal; +use crate::stdlib::convert::TryFrom; macro_rules! impl_from_int_primitive { @@ -35,3 +36,22 @@ impl_from_int_primitive!(i16); impl_from_int_primitive!(i32); impl_from_int_primitive!(i64); impl_from_int_primitive!(i128); + + +impl TryFrom for BigDecimal { + type Error = super::ParseBigDecimalError; + + #[inline] + fn try_from(n: f32) -> Result { + crate::parsing::try_parse_from_f32(n) + } +} + +impl TryFrom for BigDecimal { + type Error = super::ParseBigDecimalError; + + #[inline] + fn try_from(n: f64) -> Result { + crate::parsing::try_parse_from_f64(n) + } +} diff --git a/src/lib.rs b/src/lib.rs index d77d851..7271d8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2121,24 +2121,6 @@ impl From for BigDecimal { } -impl TryFrom for BigDecimal { - type Error = ParseBigDecimalError; - - #[inline] - fn try_from(n: f32) -> Result { - parsing::try_parse_from_f32(n) - } -} - -impl TryFrom for BigDecimal { - type Error = ParseBigDecimalError; - - #[inline] - fn try_from(n: f64) -> Result { - parsing::try_parse_from_f64(n) - } -} - impl FromPrimitive for BigDecimal { #[inline] fn from_i64(n: i64) -> Option { From 7f6dde94983200f06beb88a60d4376b901077a8a Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 11 Jul 2023 02:12:32 -0400 Subject: [PATCH 08/21] Move impl From to impl_convert --- src/impl_convert.rs | 23 +++++++++++++++++++++++ src/lib.rs | 21 --------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/impl_convert.rs b/src/impl_convert.rs index ff3f955..2ab308f 100644 --- a/src/impl_convert.rs +++ b/src/impl_convert.rs @@ -3,6 +3,8 @@ use crate::BigDecimal; use crate::stdlib::convert::TryFrom; +use num_bigint::BigInt; + macro_rules! impl_from_int_primitive { ($t:ty) => { @@ -55,3 +57,24 @@ impl TryFrom for BigDecimal { crate::parsing::try_parse_from_f64(n) } } + + +impl From for BigDecimal { + fn from(int_val: BigInt) -> Self { + BigDecimal { + int_val: int_val, + scale: 0, + } + } +} + +// Anything that may be a big-integer paired with a scale +// parameter may be a bigdecimal +impl> From<(T, i64)> for BigDecimal { + fn from((int_val, scale): (T, i64)) -> Self { + Self { + int_val: int_val.into(), + scale: scale, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7271d8b..d6b82f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2100,27 +2100,6 @@ impl ToPrimitive for BigDecimal { } -impl From<(BigInt, i64)> for BigDecimal { - #[inline] - fn from((int_val, scale): (BigInt, i64)) -> Self { - BigDecimal { - int_val: int_val, - scale: scale, - } - } -} - -impl From for BigDecimal { - #[inline] - fn from(int_val: BigInt) -> Self { - BigDecimal { - int_val: int_val, - scale: 0, - } - } -} - - impl FromPrimitive for BigDecimal { #[inline] fn from_i64(n: i64) -> Option { From 69efb943fe2cf057596728ea27bcf8ceb75facae Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 11 Jul 2023 02:19:41 -0400 Subject: [PATCH 09/21] Move num-trait implementations to impl_num --- src/impl_num.rs | 165 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 146 ++---------------------------------------- 2 files changed, 169 insertions(+), 142 deletions(-) create mode 100644 src/impl_num.rs diff --git a/src/impl_num.rs b/src/impl_num.rs new file mode 100644 index 0000000..4953cd1 --- /dev/null +++ b/src/impl_num.rs @@ -0,0 +1,165 @@ +//! Code for num_traits + +use num_traits::{Num, FromPrimitive, ToPrimitive, AsPrimitive}; +use num_bigint::{BigInt, Sign, ToBigInt}; + +use stdlib::str::FromStr; +use stdlib::string::{String, ToString}; +use stdlib::convert::TryFrom; +use stdlib::ops::Neg; + +use crate::BigDecimal; +use crate::ParseBigDecimalError; + +#[cfg(not(feature = "std"))] +// f64::powi is only available in std, no_std must use libm +fn powi(x: f64, n: f64) -> f64 { + libm::pow(x, n) +} + +#[cfg(feature = "std")] +fn powi(x: f64, n: i32) -> f64 { + x.powi(n) +} + +impl Num for BigDecimal { + type FromStrRadixErr = ParseBigDecimalError; + + /// Creates and initializes a BigDecimal. + #[inline] + fn from_str_radix(s: &str, radix: u32) -> Result { + if radix != 10 { + return Err(ParseBigDecimalError::Other(String::from( + "The radix for decimal MUST be 10", + ))); + } + + let exp_separator: &[_] = &['e', 'E']; + + // split slice into base and exponent parts + let (base_part, exponent_value) = match s.find(exp_separator) { + // exponent defaults to 0 if (e|E) not found + None => (s, 0), + + // split and parse exponent field + Some(loc) => { + // slice up to `loc` and 1 after to skip the 'e' char + let (base, exp) = (&s[..loc], &s[loc + 1..]); + + // special consideration for rust 1.0.0 which would not + // parse a leading '+' + let exp = match exp.chars().next() { + Some('+') => &exp[1..], + _ => exp, + }; + + (base, i64::from_str(exp)?) + } + }; + + // TEMPORARY: Test for emptiness - remove once BigInt supports similar error + if base_part.is_empty() { + return Err(ParseBigDecimalError::Empty); + } + + // split decimal into a digit string and decimal-point offset + let (digits, decimal_offset): (String, _) = match base_part.find('.') { + // No dot! pass directly to BigInt + None => (base_part.to_string(), 0), + + // decimal point found - necessary copy into new string buffer + Some(loc) => { + // split into leading and trailing digits + let (lead, trail) = (&base_part[..loc], &base_part[loc + 1..]); + + // copy all leading characters into 'digits' string + let mut digits = String::from(lead); + + // copy all trailing characters after '.' into the digits string + digits.push_str(trail); + + // count number of trailing digits + let trail_digits = trail.chars().filter(|c| *c != '_').count(); + + (digits, trail_digits as i64) + } + }; + + let scale = decimal_offset - exponent_value; + let big_int = BigInt::from_str_radix(&digits, radix)?; + + Ok(BigDecimal::new(big_int, scale)) + } +} + +impl ToPrimitive for BigDecimal { + fn to_i64(&self) -> Option { + match self.sign() { + Sign::Minus | Sign::Plus => self.with_scale(0).int_val.to_i64(), + Sign::NoSign => Some(0), + } + } + fn to_i128(&self) -> Option { + match self.sign() { + Sign::Minus | Sign::Plus => self.with_scale(0).int_val.to_i128(), + Sign::NoSign => Some(0), + } + } + fn to_u64(&self) -> Option { + match self.sign() { + Sign::Plus => self.with_scale(0).int_val.to_u64(), + Sign::NoSign => Some(0), + Sign::Minus => None, + } + } + fn to_u128(&self) -> Option { + match self.sign() { + Sign::Plus => self.with_scale(0).int_val.to_u128(), + Sign::NoSign => Some(0), + Sign::Minus => None, + } + } + + fn to_f64(&self) -> Option { + self.int_val.to_f64().map(|x| x * powi(10f64, self.scale.neg().as_())) + } +} + + +impl FromPrimitive for BigDecimal { + #[inline] + fn from_i64(n: i64) -> Option { + Some(BigDecimal::from(n)) + } + + #[inline] + fn from_u64(n: u64) -> Option { + Some(BigDecimal::from(n)) + } + + #[inline] + fn from_i128(n: i128) -> Option { + Some(BigDecimal::from(n)) + } + + #[inline] + fn from_u128(n: u128) -> Option { + Some(BigDecimal::from(n)) + } + + #[inline] + fn from_f32(n: f32) -> Option { + BigDecimal::try_from(n).ok() + } + + #[inline] + fn from_f64(n: f64) -> Option { + BigDecimal::try_from(n).ok() + } +} + +impl ToBigInt for BigDecimal { + fn to_bigint(&self) -> Option { + Some(self.with_scale(0).int_val) + } +} diff --git a/src/lib.rs b/src/lib.rs index d6b82f3..5194fc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,7 +73,7 @@ use self::stdlib::str::FromStr; use self::stdlib::string::{String, ToString}; use self::stdlib::fmt; -use num_bigint::{BigInt, ParseBigIntError, Sign, ToBigInt}; +use num_bigint::{BigInt, ParseBigIntError, Sign}; use num_integer::Integer as IntegerTrait; pub use num_traits::{FromPrimitive, Num, One, Signed, ToPrimitive, Zero}; @@ -93,6 +93,9 @@ extern crate paste; // From, To, TryFrom impls mod impl_convert; +// Implementations of num_traits +mod impl_num; + mod parsing; pub mod rounding; pub use rounding::RoundingMode; @@ -1996,147 +1999,6 @@ impl fmt::Debug for BigDecimal { } } -impl Num for BigDecimal { - type FromStrRadixErr = ParseBigDecimalError; - - /// Creates and initializes a BigDecimal. - #[inline] - fn from_str_radix(s: &str, radix: u32) -> Result { - if radix != 10 { - return Err(ParseBigDecimalError::Other(String::from( - "The radix for decimal MUST be 10", - ))); - } - - let exp_separator: &[_] = &['e', 'E']; - - // split slice into base and exponent parts - let (base_part, exponent_value) = match s.find(exp_separator) { - // exponent defaults to 0 if (e|E) not found - None => (s, 0), - - // split and parse exponent field - Some(loc) => { - // slice up to `loc` and 1 after to skip the 'e' char - let (base, exp) = (&s[..loc], &s[loc + 1..]); - - // special consideration for rust 1.0.0 which would not - // parse a leading '+' - let exp = match exp.chars().next() { - Some('+') => &exp[1..], - _ => exp, - }; - - (base, i64::from_str(exp)?) - } - }; - - // TEMPORARY: Test for emptiness - remove once BigInt supports similar error - if base_part.is_empty() { - return Err(ParseBigDecimalError::Empty); - } - - // split decimal into a digit string and decimal-point offset - let (digits, decimal_offset): (String, _) = match base_part.find('.') { - // No dot! pass directly to BigInt - None => (base_part.to_string(), 0), - - // decimal point found - necessary copy into new string buffer - Some(loc) => { - // split into leading and trailing digits - let (lead, trail) = (&base_part[..loc], &base_part[loc + 1..]); - - // copy all leading characters into 'digits' string - let mut digits = String::from(lead); - - // copy all trailing characters after '.' into the digits string - digits.push_str(trail); - - // count number of trailing digits - let trail_digits = trail.chars().filter(|c| *c != '_').count(); - - (digits, trail_digits as i64) - } - }; - - let scale = decimal_offset - exponent_value; - let big_int = BigInt::from_str_radix(&digits, radix)?; - - Ok(BigDecimal::new(big_int, scale)) - } -} - -impl ToPrimitive for BigDecimal { - fn to_i64(&self) -> Option { - match self.sign() { - Sign::Minus | Sign::Plus => self.with_scale(0).int_val.to_i64(), - Sign::NoSign => Some(0), - } - } - fn to_i128(&self) -> Option { - match self.sign() { - Sign::Minus | Sign::Plus => self.with_scale(0).int_val.to_i128(), - Sign::NoSign => Some(0), - } - } - fn to_u64(&self) -> Option { - match self.sign() { - Sign::Plus => self.with_scale(0).int_val.to_u64(), - Sign::NoSign => Some(0), - Sign::Minus => None, - } - } - fn to_u128(&self) -> Option { - match self.sign() { - Sign::Plus => self.with_scale(0).int_val.to_u128(), - Sign::NoSign => Some(0), - Sign::Minus => None, - } - } - - fn to_f64(&self) -> Option { - self.int_val.to_f64().map(|x| x * 10f64.powi(-self.scale as i32)) - } -} - - -impl FromPrimitive for BigDecimal { - #[inline] - fn from_i64(n: i64) -> Option { - Some(BigDecimal::from(n)) - } - - #[inline] - fn from_u64(n: u64) -> Option { - Some(BigDecimal::from(n)) - } - - #[inline] - fn from_i128(n: i128) -> Option { - Some(BigDecimal::from(n)) - } - - #[inline] - fn from_u128(n: u128) -> Option { - Some(BigDecimal::from(n)) - } - - #[inline] - fn from_f32(n: f32) -> Option { - BigDecimal::try_from(n).ok() - } - - #[inline] - fn from_f64(n: f64) -> Option { - BigDecimal::try_from(n).ok() - } -} - -impl ToBigInt for BigDecimal { - fn to_bigint(&self) -> Option { - Some(self.with_scale(0).int_val) - } -} /// Tools to help serializing/deserializing `BigDecimal`s #[cfg(feature = "serde")] From a86fa5e9700e568c4c38bdea43d7892166df267b Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 11 Jul 2023 02:34:21 -0400 Subject: [PATCH 10/21] Impl Add<{int}> and AddAssign<{int}> in impl_ops.rs --- src/impl_ops.rs | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ 2 files changed, 83 insertions(+) create mode 100644 src/impl_ops.rs diff --git a/src/impl_ops.rs b/src/impl_ops.rs new file mode 100644 index 0000000..e44b8e9 --- /dev/null +++ b/src/impl_ops.rs @@ -0,0 +1,81 @@ +//! Implement math operations: Add,Sub, etc + +use crate::BigDecimal; +use crate::stdlib::ops::{ + Add, AddAssign, +}; + +macro_rules! impl_add_for_primitive { + ($t:ty) => { + impl_add_for_primitive!(IMPL:ADD $t); + impl_add_for_primitive!(IMPL:ADD-ASSIGN $t); + impl_add_for_primitive!(IMPL:ADD &$t); + impl_add_for_primitive!(IMPL:ADD-ASSIGN &$t); + }; + (IMPL:ADD $t:ty) => { + impl Add<$t> for BigDecimal { + type Output = BigDecimal; + + fn add(mut self, rhs: $t) -> BigDecimal { + self += rhs; + self + } + } + + impl Add<$t> for &BigDecimal { + type Output = BigDecimal; + + fn add(self, rhs: $t) -> BigDecimal { + BigDecimal::from(rhs) + self + } + } + + impl Add for $t { + type Output = BigDecimal; + + fn add(self, rhs: BigDecimal) -> BigDecimal { + rhs + self + } + } + + impl Add<&BigDecimal> for $t { + type Output = BigDecimal; + + fn add(self, rhs: &BigDecimal) -> BigDecimal { + rhs + self + } + } + }; + (IMPL:ADD-ASSIGN &$t:ty) => { + // special case for the ref types + impl AddAssign<&$t> for BigDecimal { + fn add_assign(&mut self, rhs: &$t) { + *self += *rhs; + } + } + }; + (IMPL:ADD-ASSIGN $t:ty) => { + impl AddAssign<$t> for BigDecimal { + fn add_assign(&mut self, rhs: $t) { + if rhs == 0 { + // no-op + } else if self.scale == 0 { + self.int_val += rhs; + } else { + *self += BigDecimal::from(rhs); + } + } + } + }; +} + +impl_add_for_primitive!(u8); +impl_add_for_primitive!(u16); +impl_add_for_primitive!(u32); +impl_add_for_primitive!(u64); +impl_add_for_primitive!(u128); +impl_add_for_primitive!(i8); +impl_add_for_primitive!(i16); +impl_add_for_primitive!(i32); +impl_add_for_primitive!(i64); +impl_add_for_primitive!(i128); diff --git a/src/lib.rs b/src/lib.rs index 5194fc1..dd59629 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,6 +92,8 @@ extern crate paste; // From, To, TryFrom impls mod impl_convert; +// Add, Sub, etc... +mod impl_ops; // Implementations of num_traits mod impl_num; From ec2a606eb91b9011f013f71853f023a77152f9be Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 11 Jul 2023 02:58:48 -0400 Subject: [PATCH 11/21] Impl Sub<{int}> and SubAssign<{int}> --- src/impl_ops.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/impl_ops.rs b/src/impl_ops.rs index e44b8e9..4ecbc99 100644 --- a/src/impl_ops.rs +++ b/src/impl_ops.rs @@ -3,6 +3,8 @@ use crate::BigDecimal; use crate::stdlib::ops::{ Add, AddAssign, + Sub, SubAssign, + Neg, }; macro_rules! impl_add_for_primitive { @@ -79,3 +81,78 @@ impl_add_for_primitive!(i16); impl_add_for_primitive!(i32); impl_add_for_primitive!(i64); impl_add_for_primitive!(i128); + + +macro_rules! impl_sub_for_primitive { + ($t:ty) => { + impl_sub_for_primitive!(IMPL:SUB $t); + impl_sub_for_primitive!(IMPL:SUB-ASSIGN $t); + impl_sub_for_primitive!(IMPL:SUB &$t); + impl_sub_for_primitive!(IMPL:SUB-ASSIGN &$t); + }; + (IMPL:SUB $t:ty) => { + impl Sub<$t> for BigDecimal { + type Output = BigDecimal; + + fn sub(mut self, rhs: $t) -> BigDecimal { + self -= rhs; + self + } + } + + impl Sub<$t> for &BigDecimal { + type Output = BigDecimal; + + fn sub(self, rhs: $t) -> BigDecimal { + let res = BigDecimal::from(rhs).neg(); + res + self + } + } + + impl Sub for $t { + type Output = BigDecimal; + + fn sub(self, rhs: BigDecimal) -> BigDecimal { + rhs.neg() + self + } + } + + impl Sub<&BigDecimal> for $t { + type Output = BigDecimal; + + fn sub(self, rhs: &BigDecimal) -> BigDecimal { + rhs.neg() + self + } + } + }; + (IMPL:SUB-ASSIGN &$t:ty) => { + impl SubAssign<&$t> for BigDecimal { + fn sub_assign(&mut self, rhs: &$t) { + *self -= *rhs; + } + } + }; + (IMPL:SUB-ASSIGN $t:ty) => { + impl SubAssign<$t> for BigDecimal { + fn sub_assign(&mut self, rhs: $t) { + if self.scale == 0 { + self.int_val -= rhs; + } else { + *self -= BigDecimal::from(rhs); + } + } + } + }; +} + + +impl_sub_for_primitive!(u8); +impl_sub_for_primitive!(u16); +impl_sub_for_primitive!(u32); +impl_sub_for_primitive!(u64); +impl_sub_for_primitive!(u128); +impl_sub_for_primitive!(i8); +impl_sub_for_primitive!(i16); +impl_sub_for_primitive!(i32); +impl_sub_for_primitive!(i64); +impl_sub_for_primitive!(i128); From 2800aa309cc893efc702572c02018d436703afc2 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Tue, 11 Jul 2023 03:12:35 -0400 Subject: [PATCH 12/21] Impl Mul<{int}> and MulAssign<{int}> --- src/impl_ops.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/impl_ops.rs b/src/impl_ops.rs index 4ecbc99..4eb6063 100644 --- a/src/impl_ops.rs +++ b/src/impl_ops.rs @@ -4,9 +4,13 @@ use crate::BigDecimal; use crate::stdlib::ops::{ Add, AddAssign, Sub, SubAssign, + Mul, MulAssign, Neg, }; +use num_traits::{Zero, One}; + + macro_rules! impl_add_for_primitive { ($t:ty) => { impl_add_for_primitive!(IMPL:ADD $t); @@ -156,3 +160,73 @@ impl_sub_for_primitive!(i16); impl_sub_for_primitive!(i32); impl_sub_for_primitive!(i64); impl_sub_for_primitive!(i128); + + +macro_rules! impl_mul_for_primitive { + ($t:ty) => { + impl_mul_for_primitive!(IMPL:MUL $t); + impl_mul_for_primitive!(IMPL:MUL-ASSIGN $t); + impl_mul_for_primitive!(IMPL:MUL &$t); + impl_mul_for_primitive!(IMPL:MUL-ASSIGN &$t); + }; + (IMPL:MUL $t:ty) => { + impl Mul<$t> for BigDecimal { + type Output = BigDecimal; + + fn mul(mut self, rhs: $t) -> BigDecimal { + self *= rhs; + self + } + } + + impl Mul<$t> for &BigDecimal { + type Output = BigDecimal; + + fn mul(self, rhs: $t) -> BigDecimal { + let res = BigDecimal::from(rhs); + res * self + } + } + + impl Mul for $t { + type Output = BigDecimal; + + fn mul(self, rhs: BigDecimal) -> BigDecimal { + rhs * self + } + } + + impl Mul<&BigDecimal> for $t { + type Output = BigDecimal; + + fn mul(self, rhs: &BigDecimal) -> BigDecimal { + rhs * self + } + } + }; + (IMPL:MUL-ASSIGN $t:ty) => { + impl MulAssign<$t> for BigDecimal { + fn mul_assign(&mut self, rhs: $t) { + if rhs.is_zero() { + *self = BigDecimal::zero() + } else if rhs.is_one() { + // no-op + } else { + *self *= BigDecimal::from(rhs); + } + } + } + }; +} + + +impl_mul_for_primitive!(u8); +impl_mul_for_primitive!(u16); +impl_mul_for_primitive!(u32); +impl_mul_for_primitive!(u64); +impl_mul_for_primitive!(u128); +impl_mul_for_primitive!(i8); +impl_mul_for_primitive!(i16); +impl_mul_for_primitive!(i32); +impl_mul_for_primitive!(i64); +impl_mul_for_primitive!(i128); From 50ec8e4470897b662b052d333c422f5bd7006bc7 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 22:18:39 -0400 Subject: [PATCH 13/21] Move impl Div<{primitive}> to impl_ops --- src/impl_ops.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 - src/macros.rs | 186 ---------------------------------------------- 3 files changed, 193 insertions(+), 187 deletions(-) diff --git a/src/impl_ops.rs b/src/impl_ops.rs index 4eb6063..c7b0e37 100644 --- a/src/impl_ops.rs +++ b/src/impl_ops.rs @@ -5,9 +5,13 @@ use crate::stdlib::ops::{ Add, AddAssign, Sub, SubAssign, Mul, MulAssign, + Div, DivAssign, Neg, }; + +use crate::stdlib::convert::TryFrom; + use num_traits::{Zero, One}; @@ -230,3 +234,192 @@ impl_mul_for_primitive!(i16); impl_mul_for_primitive!(i32); impl_mul_for_primitive!(i64); impl_mul_for_primitive!(i128); + +macro_rules! impl_div_for_primitive { + (f32) => { + impl_div_for_primitive!(IMPL:DIV:FLOAT f32); + impl_div_for_primitive!(IMPL:DIV:REF &f32); + }; + (f64) => { + impl_div_for_primitive!(IMPL:DIV:FLOAT f64); + impl_div_for_primitive!(IMPL:DIV:REF &f64); + }; + ($t:ty) => { + impl_div_for_primitive!(IMPL:DIV $t); + impl_div_for_primitive!(IMPL:DIV:REF &$t); + impl_div_for_primitive!(IMPL:DIV-ASSIGN $t); + }; + (IMPL:DIV $t:ty) => { + impl Div<$t> for BigDecimal { + type Output = BigDecimal; + + fn div(self, denom: $t) -> BigDecimal { + if denom.is_one() { + self + } else if denom.checked_neg().is_some_and(|n| n == 1) { + self.neg() + } else if denom.clone() == 2 { + self.half() + } else if denom.checked_neg().is_some_and(|n| n == 2) { + self.half().neg() + } else { + self / BigDecimal::from(denom) + } + } + } + + impl Div<$t> for &BigDecimal { + type Output = BigDecimal; + + fn div(self, denom: $t) -> BigDecimal { + self.clone() / denom + } + } + + impl Div for $t { + type Output = BigDecimal; + + fn div(self, denom: BigDecimal) -> BigDecimal { + if self.is_one() { + denom.inverse() + } else { + BigDecimal::from(self) / denom + } + } + } + + impl Div<&BigDecimal> for $t { + type Output = BigDecimal; + + fn div(self, denom: &BigDecimal) -> BigDecimal { + BigDecimal::from(self) / denom + } + } + }; + (IMPL:DIV-ASSIGN $t:ty) => { + impl DivAssign<$t> for BigDecimal { + fn div_assign(&mut self, rhs: $t) { + if rhs.is_zero() { + *self = BigDecimal::zero() + } else if rhs.is_one() { + // no-op + } else { + *self = self.clone() / BigDecimal::from(rhs); + } + } + } + }; + (IMPL:DIV:REF $t:ty) => { + impl Div<$t> for BigDecimal { + type Output = BigDecimal; + + fn div(self, denom: $t) -> BigDecimal { + self / *denom + } + } + + impl Div for $t { + type Output = BigDecimal; + + fn div(self, denom: BigDecimal) -> Self::Output { + *self / denom + } + } + + impl Div<&BigDecimal> for $t { + type Output = BigDecimal; + + fn div(self, denom: &BigDecimal) -> Self::Output { + *self / denom + } + } + + impl DivAssign<$t> for BigDecimal { + fn div_assign(&mut self, denom: $t) { + self.div_assign(*denom) + } + } + }; + (IMPL:DIV:FLOAT $t:ty) => { + impl Div<$t> for BigDecimal { + type Output = BigDecimal; + + fn div(self, denom: $t) -> BigDecimal { + if !denom.is_normal() { + BigDecimal::zero() + } else if denom == (1.0 as $t) { + self + } else if denom == (-1.0 as $t) { + self.neg() + } else if denom == (2.0 as $t) { + self.half() + } else if denom == (-2.0 as $t) { + self.half().neg() + } else { + self / BigDecimal::try_from(denom).unwrap() + } + } + } + + impl Div<$t> for &BigDecimal { + type Output = BigDecimal; + + fn div(self, denom: $t) -> BigDecimal { + self.clone() / denom + } + } + + impl Div for $t { + type Output = BigDecimal; + + fn div(self, denom: BigDecimal) -> Self::Output { + if !self.is_normal() { + BigDecimal::zero() + } else if self.is_one() { + denom.inverse() + } else { + BigDecimal::try_from(self).unwrap() / denom + } + } + } + + impl Div<&BigDecimal> for $t { + type Output = BigDecimal; + + fn div(self, denom: &BigDecimal) -> Self::Output { + if !self.is_normal() { + BigDecimal::zero() + } else if self.is_one() { + denom.inverse() + } else { + BigDecimal::try_from(self).unwrap() / denom + } + } + } + + impl DivAssign<$t> for BigDecimal { + fn div_assign(&mut self, denom: $t) { + if !denom.is_normal() { + *self = BigDecimal::zero() + } else { + *self = self.clone() / BigDecimal::try_from(denom).unwrap() + }; + } + } + }; +} + + +impl_div_for_primitive!(u8); +impl_div_for_primitive!(u16); +impl_div_for_primitive!(u32); +impl_div_for_primitive!(u64); +impl_div_for_primitive!(u128); +impl_div_for_primitive!(i8); +impl_div_for_primitive!(i16); +impl_div_for_primitive!(i32); +impl_div_for_primitive!(i64); +impl_div_for_primitive!(i128); + +impl_div_for_primitive!(f32); +impl_div_for_primitive!(f64); diff --git a/src/lib.rs b/src/lib.rs index dd59629..5370f9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1650,7 +1650,6 @@ impl MulAssign for BigDecimal { } } -impl_div_for_primitives!(); #[inline(always)] fn impl_division(mut num: BigInt, den: &BigInt, mut scale: i64, max_precision: u64) -> BigDecimal { diff --git a/src/macros.rs b/src/macros.rs index 64e8f05..e1c75bb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -66,189 +66,3 @@ macro_rules! forward_val_assignop { } }; } - -macro_rules! impl_div_for_uint_primitive { - // (impl $imp:ident for $res:ty, $method:ident) => { - ($res:ty) => { - impl<'a> Div<$res> for &'a BigDecimal { - type Output = BigDecimal; - - #[inline] - fn div(self, den: $res) -> Self::Output { - if den == 1 { - self.clone() - } else if den == 2 { - self.half() - } else { - self / BigDecimal::from(den) - } - } - } - - impl<'a> Div<&'a BigDecimal> for $res { - type Output = BigDecimal; - - #[inline(always)] - fn div(self, den: &'a BigDecimal) -> Self::Output { - BigDecimal::from(self) / den - } - } - - impl Div for $res { - type Output = BigDecimal; - - #[inline(always)] - fn div(self, den: BigDecimal) -> Self::Output { - BigDecimal::from(self) / den - } - } - }; -} - -macro_rules! impl_div_for_int_primitive { - // (impl $imp:ident for $res:ty, $method:ident) => { - ($res:ty) => { - impl<'a> Div<$res> for BigDecimal { - type Output = BigDecimal; - - #[inline(always)] - fn div(self, den: $res) -> Self::Output { - if den < 0 { - -Div::div(self, -den) - } else if den == 1 { - self - } else if den == 2 { - self.half() - } else { - self / BigDecimal::from(den) - } - } - } - - impl<'a> Div<$res> for &'a BigDecimal { - type Output = BigDecimal; - - #[inline(always)] - fn div(self, den: $res) -> Self::Output { - if den < 0 { - -Div::div(self, -den) - } else if den == 1 { - self.clone() - } else if den == 2 { - self.half() - } else { - self / BigDecimal::from(den) - } - } - } - - impl<'a> Div<&'a BigDecimal> for $res { - type Output = BigDecimal; - - #[inline(always)] - fn div(self, den: &'a BigDecimal) -> Self::Output { - match (self < 0, den.is_negative()) { - (true, true) => -self / -den, - (true, false) => (-self / den).neg(), - (false, true) => (-self / den.abs()), - (false, false) => BigDecimal::from(self) / den, - } - } - } - - impl Div for $res { - type Output = BigDecimal; - - #[inline(always)] - fn div(self, den: BigDecimal) -> Self::Output { - match (self < 0, den.is_negative()) { - (true, true) => -self / -den, - (true, false) => (-self / den).neg(), - (false, true) => (-self / den.abs()), - (false, false) => BigDecimal::from(self) / den, - } - } - } - }; -} - -macro_rules! impl_div_for_float_primitive { - // (impl $imp:ident for $res:ty, $method:ident) => { - ($res:ty) => { - impl<'a> Div<$res> for &'a BigDecimal { - type Output = BigDecimal; - - #[inline] - #[allow(clippy::float_cmp)] - fn div(self, den: $res) -> Self::Output { - if den.is_nan() { - BigDecimal::zero() - } else if den == 1.0 { - self.clone() - } else if den == 0.5 { - self.double() - } else if den == 2.0 { - self.half() - } else if den == -1.0 { - -self - } else if den < 0.0 && self.is_positive() { - -(self / -den) - } else { - // Unwrap is safe, because `is_nan` checked above - self / BigDecimal::try_from(den).unwrap() - } - } - } - - impl<'a> Div<&'a BigDecimal> for $res { - type Output = BigDecimal; - #[inline(always)] - fn div(self, den: &'a BigDecimal) -> Self::Output { - if self.is_nan() { - BigDecimal::zero() - } else { - BigDecimal::try_from(self).unwrap() / den - } - } - } - - impl Div for $res { - type Output = BigDecimal; - #[inline(always)] - fn div(self, den: BigDecimal) -> Self::Output { - if self.is_nan() { - BigDecimal::zero() - } else { - BigDecimal::try_from(self).unwrap() / den - } - } - } - }; -} - -macro_rules! forward_primitive_types { - (floats => $macro_name:ident) => { - $macro_name!(f32); - $macro_name!(f64); - }; - (ints => $macro_name:ident) => { - $macro_name!(i8); - $macro_name!(i16); - $macro_name!(i32); - $macro_name!(i64); - }; - (uints => $macro_name:ident) => { - $macro_name!(u8); - $macro_name!(u16); - $macro_name!(u32); - $macro_name!(u64); - }; -} - -macro_rules! impl_div_for_primitives { - () => { - forward_primitive_types!(floats => impl_div_for_float_primitive); - forward_primitive_types!(ints => impl_div_for_int_primitive); - forward_primitive_types!(uints => impl_div_for_uint_primitive); - }; -} From 65f9f8f8a450049dc49c8b6eb306ce54e67ca862 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 23:11:07 -0400 Subject: [PATCH 14/21] Fix some rustfmt issues --- src/rounding.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/rounding.rs b/src/rounding.rs index a1053d5..bbce7f1 100644 --- a/src/rounding.rs +++ b/src/rounding.rs @@ -172,7 +172,13 @@ impl RoundingMode { /// Calculation of pair of digits from full number, and the replacement of that number /// should be handled separately /// - pub fn round_u32(&self, at_digit: stdlib::num::NonZeroU8, sign: Sign, value: u32, trailing_zeros: bool) -> u32 { + pub fn round_u32( + &self, + at_digit: stdlib::num::NonZeroU8, + sign: Sign, + value: u32, + trailing_zeros: bool, + ) -> u32 { let shift = 10u32.pow(at_digit.get() as u32 - 1); let splitter = shift * 10; @@ -448,7 +454,7 @@ mod test_round_u32 { } mod case_neg_35488622_000x { - use super::*; + use super::*; // ...000x indicates non-zero trailing digit define_test_input!(-35488622 ...000x); From 6515af3d2993f808c7297020a0a570468fcaf069 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Mon, 10 Jul 2023 19:55:29 -0400 Subject: [PATCH 15/21] Add support for proptests --- .gitignore | 2 ++ Cargo.toml | 3 +++ scripts/bigdecimal-property-tests | 19 +++++++++++++++++++ src/lib.rs | 7 +++++++ src/lib.tests.property-tests.rs | 0 5 files changed, 31 insertions(+) create mode 100755 scripts/bigdecimal-property-tests create mode 100644 src/lib.tests.property-tests.rs diff --git a/.gitignore b/.gitignore index 62216a9..8af57e8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ builds/ *venv*/ .python-version *.ipynb + +proptest-regressions diff --git a/Cargo.toml b/Cargo.toml index bb51afb..302c713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ siphasher = { version = "0.3.10", default-features = false } # BENCH: oorandom = { version = "11.1.3" } # BENCH: lazy_static = { version = "1" } +# Only required for property testing - incompatible with older versions of rust +# PROPERTY-TESTS: proptest = "1" + [features] default = ["std"] string-only = [] diff --git a/scripts/bigdecimal-property-tests b/scripts/bigdecimal-property-tests new file mode 100755 index 0000000..bce2e79 --- /dev/null +++ b/scripts/bigdecimal-property-tests @@ -0,0 +1,19 @@ +#!/bin/sh +# +# Run commands with property-tests enabled for bigdecimal crate +# +# Tests are defined in src/lib.tests.property-test.rs +# + +# enable property-test dependencies in Cargo +sed -i.bak -e 's|# PROPERTY-TESTS: ||' Cargo.toml + +# include the property-test file in lib.rs +sed -i.bak -e 's|// ::PROPERTY-TESTS:: ||' src/lib.rs + +# Run commands +"$@" + +# Restore Cargo.toml with backup +mv Cargo.toml.bak Cargo.toml +mv src/lib.rs.bak src/lib.rs diff --git a/src/lib.rs b/src/lib.rs index 5370f9a..a77801b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1116,6 +1116,7 @@ impl One for BigDecimal { } } + impl Add for BigDecimal { type Output = BigDecimal; @@ -3254,3 +3255,9 @@ mod test_with_scale_round { include!("lib.tests.with_scale_round.rs"); } + +// enable these tests with scripts/bigdecimal-property-tests +// ::PROPERTY-TESTS:: #[cfg(test)] #[macro_use] extern crate proptest; +// ::PROPERTY-TESTS:: #[cfg(test)] mod property_tests { +// ::PROPERTY-TESTS:: use super::*; use paste::paste; +// ::PROPERTY-TESTS:: include!("lib.tests.property-tests.rs"); } diff --git a/src/lib.tests.property-tests.rs b/src/lib.tests.property-tests.rs new file mode 100644 index 0000000..e69de29 From 1d4f1d8a0000c65c5b0de566f136eff1719dedfb Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 20:12:34 -0400 Subject: [PATCH 16/21] Add property-tests file --- src/lib.tests.property-tests.rs | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/lib.tests.property-tests.rs b/src/lib.tests.property-tests.rs index e69de29..4327bda 100644 --- a/src/lib.tests.property-tests.rs +++ b/src/lib.tests.property-tests.rs @@ -0,0 +1,120 @@ +// Property tests to be included by lib.rs (if enabled) + + +mod arithmetic { + use super::*; + + macro_rules! impl_test { + ($t:ty) => { + paste! { proptest! { + #[test] + fn [< add_ref $t >](n: $t, m: i128, e: i8) { + let d = BigDecimal::new(m.into(), e as i64); + let sum = n + &d; + + let s1 = &d + n; + let s2 = d.clone() + n; + + prop_assert_eq!(&sum, &s1); + prop_assert_eq!(&sum, &s2); + + let mut s = d; + s += n; + prop_assert_eq!(sum, s); + } + + #[test] + fn [< sub_ $t >](n: $t, m: i128, e: i8) { + let d = BigDecimal::new(m.into(), e as i64); + let diff_n_d = n - &d; + let diff_d_n = d.clone() - n; + prop_assert_eq!(&diff_n_d, &diff_d_n.neg()); + + let mut a = d.clone(); + a -= n; + prop_assert_eq!(&a, &diff_n_d.neg()); + } + + #[test] + fn [< mul_ $t >](n: $t, m: i128, e: i8) { + let d = BigDecimal::new(m.into(), e as i64); + let prod_n_d = n * &d; + let prod_d_n = d.clone() * n; + prop_assert_eq!(&prod_n_d, &prod_d_n); + + let mut r = d.clone(); + r *= n; + prop_assert_eq!(&prod_n_d, &r); + + let r = d.neg() * n; + prop_assert_eq!(prod_n_d.neg(), r); + } + + #[test] + fn [< div_ $t >](n: $t, m: i128, e: i8) { + prop_assume!(m != 0); + + let d = BigDecimal::new(m.into(), e as i64); + let quotient_n_ref_d = n / &d; + let quotient_n_d = n / d.clone(); + prop_assert_eq!("ient_n_ref_d, "ient_n_d); + + let prod = quotient_n_d * &d; + let diff = n - ∏ + // prop_assert!(dbg!(diff.scale) > 99); + prop_assert!(diff.abs() < BigDecimal::new(1.into(), 60)); + } + } } + }; + (float-div $t:ty) => { + paste! { proptest! { + #[test] + fn [< div_ $t >](n: $t, m: i128, e: i8) { + prop_assume!(m != 0); + + let d = BigDecimal::new(m.into(), e as i64); + let quotient_n_ref_d = n / &d; + let quotient_n_d = n / d.clone(); + prop_assert_eq!("ient_n_ref_d, "ient_n_d); + + let d = BigDecimal::new(m.into(), e as i64); + let quotient_ref_d_n = &d / n; + let quotient_d_n = d.clone() / n; + prop_assert_eq!("ient_ref_d_n, "ient_d_n); + + let mut q = d.clone(); + q /= n; + prop_assert_eq!(&q, "ient_d_n); + } + } } + }; + } + + impl_test!(u8); + impl_test!(u16); + impl_test!(u32); + impl_test!(u64); + impl_test!(u128); + + impl_test!(i8); + impl_test!(i16); + impl_test!(i32); + impl_test!(i64); + impl_test!(i128); + + impl_test!(float-div f32); + impl_test!(float-div f64); + + proptest! { + #[test] + fn square(f: f32) { + // ignore non-normal numbers + prop_assume!(f.is_normal()); + + let n: BigDecimal = BigDecimal::from_f32(f).unwrap(); + let n_times_n = &n * &n; + + prop_assert_eq!(n_times_n, n.square()) + } + } +} From 33232cedd84476d1311b40f8df21e776a384cafc Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 22:45:20 -0400 Subject: [PATCH 17/21] Enable property tests in gitlab-ci.yml --- .gitlab-ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c4d2c80..f938a8a 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -116,6 +116,9 @@ cargo:check: variables: RUST_CACHE_KEY: "stable" <<: *script-cargo-check + script: + # enable property tests for the stable 'pipeline' + - scripts/bigdecimal-property-tests cargo check --tests cargo:clippy: stage: check @@ -153,6 +156,10 @@ cargo:build-stable: variables: RUST_CACHE_KEY: "stable" <<: *script-cargo-build + script: + # enable property tests for the stable 'pipeline' + - scripts/bigdecimal-property-tests cargo build --tests + cargo:test-stable: stage: test @@ -162,6 +169,9 @@ cargo:test-stable: variables: RUST_CACHE_KEY: "stable" <<: *script-cargo-test + script: + # enable property tests for the stable 'pipeline' + - scripts/bigdecimal-property-tests cargo test cargo:build:no-std: From 35108702cd224f70135b804c7d1c0911a9722242 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 23:19:59 -0400 Subject: [PATCH 18/21] Pin serde json to 1.0.96 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 302c713..d5278a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ serde = { version = "1.0", optional = true, default-features = false } [dev-dependencies] paste = "1" -serde_json = "1.0" +serde_json = "<1.0.101" siphasher = { version = "0.3.10", default-features = false } # The following dev-dependencies are only required for benchmarking # (use the `benchmark-bigdecimal` script to uncomment these and run benchmarks) From 208831c06a662979ce73adb2c534cc857390b123 Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 23:33:57 -0400 Subject: [PATCH 19/21] Add autocfg to probe rust version --- Cargo.toml | 3 +++ build.rs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d5278a8..e6ca6fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,9 @@ siphasher = { version = "0.3.10", default-features = false } # Only required for property testing - incompatible with older versions of rust # PROPERTY-TESTS: proptest = "1" +[build-dependencies] +autocfg = "1" + [features] default = ["std"] string-only = [] diff --git a/build.rs b/build.rs index b520ea6..7b35ce4 100644 --- a/build.rs +++ b/build.rs @@ -1,10 +1,15 @@ #![allow(clippy::style)] +extern crate autocfg; use std::env; use std::path::PathBuf; + fn main() -> std::io::Result<()> { + let ac = autocfg::new(); + ac.emit_rustc_version(1, 70); + let outdir = match std::env::var_os("OUT_DIR") { None => return Ok(()), Some(outdir) => outdir, From ccb6ccf6bb2b01c613ac64509778fc62a16561af Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 23:39:44 -0400 Subject: [PATCH 20/21] Add alternative implementation of Div for older versions --- src/impl_ops.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/impl_ops.rs b/src/impl_ops.rs index c7b0e37..8e91e60 100644 --- a/src/impl_ops.rs +++ b/src/impl_ops.rs @@ -253,6 +253,7 @@ macro_rules! impl_div_for_primitive { impl Div<$t> for BigDecimal { type Output = BigDecimal; + #[cfg(rustc_1_70)] // Option::is_some_and fn div(self, denom: $t) -> BigDecimal { if denom.is_one() { self @@ -266,6 +267,21 @@ macro_rules! impl_div_for_primitive { self / BigDecimal::from(denom) } } + + #[cfg(not(rustc_1_70))] + fn div(self, denom: $t) -> BigDecimal { + if denom.is_one() { + self + } else if denom.checked_neg().map(|n| n == 1).unwrap_or(false) { + self.neg() + } else if denom.clone() == 2 { + self.half() + } else if denom.checked_neg().map(|n| n == 2).unwrap_or(false) { + self.half().neg() + } else { + self / BigDecimal::from(denom) + } + } } impl Div<$t> for &BigDecimal { From 1fc48f58fde25607a27a0ab083b788c4abf8420e Mon Sep 17 00:00:00 2001 From: Andrew Kubera Date: Wed, 12 Jul 2023 23:47:25 -0400 Subject: [PATCH 21/21] Version 0.4.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e6ca6fc..25c2935 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.1+dev" +version = "0.4.1" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal"