diff --git a/.circleci/config.yml b/.circleci/config.yml index bf4eb2b..01f2348 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,20 +1,23 @@ version: 2.1 orbs: -# codecov: codecov/codecov@3.2.4 - rust: circleci/rust@1.6.0 + # codecov: codecov/codecov@3.3.0 + rust: circleci/rust@1.6.1 jobs: build-and-test: parameters: rust-version: type: string - default: "1.69.0" + default: "1.75.0" debian-version: type: string - default: "buster" + default: "bookworm" rust-features: type: string default: "--all-targets" + proptest-enable: + type: boolean + default: false docker: - image: rust:<< parameters.rust-version >>-<< parameters.debian-version >> environment: @@ -25,9 +28,18 @@ jobs: - run: name: Rust Version command: rustc --version; cargo --version + - when: + condition: << parameters.proptest-enable >> + steps: + - run: + name: Enable Running Property Tests + command: scripts/bigdecimal-property-tests enable + - run: + name: Generate cargo.lock + command: cargo generate-lockfile - restore_cache: keys: - - bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.toml" }} + - bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.lock" }} - bigdecimal-cargo- - run: name: Check @@ -35,7 +47,7 @@ jobs: - save_cache: paths: - /usr/local/cargo - key: bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.toml" }} + key: bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.lock" }} - run: name: Build command: cargo build << parameters.rust-features >> @@ -49,7 +61,7 @@ jobs: type: string debian-version: type: string - default: "bullseye" + default: "bookworm" machine: true steps: - checkout @@ -65,54 +77,93 @@ jobs: sh -c 'cargo test -q --no-run && kcov-rust && upload-kcov-results-to-codecov' - store_artifacts: path: target/cov - # - store_test_results: - # path: target lint-check: docker: - - image: cimg/rust:1.69 + - image: cimg/rust:1.75 steps: - checkout + - run: + name: Generate cargo.lock + command: cargo generate-lockfile - rust/build: with_cache: false # - rust/format - # - rust/clippy + - rust/clippy - rust/test - run: name: Build examples command: cargo build --examples + cargo-semver-check: + docker: + - image: "akubera/rust:stable" + steps: + - checkout + - run: + name: Tool Versions + command: > + rustc --version + && cargo --version + && cargo semver-checks --version + - run: + name: cargo semver-checks + command: cargo semver-checks --verbose + - run: + name: cargo semver-checks (no-std) + command: cargo semver-checks --verbose --only-explicit-features + workflows: version: 2 cargo:build-and-test: jobs: + - rust/lint-test-build: + name: "lint-test-build:stable" + release: true + version: "1.75" + pre-steps: + - checkout + - run: + command: cargo generate-lockfile + - rust/lint-test-build: + name: "lint-test-build:1.56" + release: true + version: "1.56" + - lint-check + + - build-and-test: + name: build-and-test:MSRV + rust-version: "1.43.1" + debian-version: "buster" + - build-and-test: - matrix: - parameters: - rust-version: - - "1.43.1" - - "1.54.0" + name: build-and-test:MSRV:serde + rust-version: "1.43.1" + debian-version: "buster" + rust-features: "--all-targets --features='serde'" - build-and-test: - name: build-and-test:latest - debian-version: "bullseye" + name: build-and-test:latest - build-and-test: - matrix: - parameters: - rust-version: - - "1.43.1" - - "1.69.0" - rust-features: - - "--features='serde'" - - "--features='serde,string-only'" + name: build-and-test:latest:serde + rust-features: "--all-targets --features='serde'" + proptest-enable: true - build-and-test: - name: build-and-test:no-default-features - rust-features: "--no-default-features" + name: build-and-test:no_std + rust-features: "--no-default-features" + + - build-and-test: + name: build-and-test:serde+no_std + rust-features: "--no-default-features --features='serde'" + + - cargo-semver-check: + requires: + - build-and-test:latest:serde - upload-coverage: - rust-version: "1.69.0" + rust-version: "1.75.0" requires: - - build-and-test:latest + - build-and-test:latest:serde diff --git a/Cargo.toml b/Cargo.toml index 49bd37e..b388e00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bigdecimal" -version = "0.4.2" +version = "0.4.3" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal" @@ -28,7 +28,7 @@ serde = { version = "1.0", optional = true, default-features = false } [dev-dependencies] paste = "1" -serde_json = "<1.0.101" +serde_test = "<1.0.176" 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) diff --git a/README.md b/README.md index 2644046..90477b8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,17 @@ -Arbitary-precision decimal numbers implemented in pure Rust. +Arbitrary-precision decimal numbers implemented in pure Rust. + +## Community + +Join the conversation on Zulip: https://bigdecimal-rs.zulipchat.com + +Please share important stuff like use-cases, issues, benchmarks, and +naming-convention preferences. + +This project is currently being re-written, so if performance or flexibility +is lacking, check again soon and it may be fixed. ## Usage @@ -41,6 +51,25 @@ sqrt(2) = 1.41421356237309504880168872420969807856967187537694807317667973799073 ``` +### Compile-Time Configuration + +You can set a few default parameters at _compile-time_ via environment variables: + +| Environment Variable | Default | +|-------------------------------------------------|------------| +| `RUST_BIGDECIMAL_DEFAULT_PRECISION` | 100 | +| `RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE` | `HalfEven` | +| `RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD` | 5 | + +These allow setting the default [Context] fields globally without incurring a runtime lookup, +or having to pass Context parameters through all calculations. +(If you want runtime control over these fields, you will have to pass Contexts to your functions.) +Examine [build.rs] for how those are converted to constants in the code (if interested). + +[Context]: https://docs.rs/bigdecimal/latest/bigdecimal/struct.Context.html +[build.rs]: ./build.rs + + #### Default precision Default precision may be set at compile time with the environment variable `RUST_BIGDECIMAL_DEFAULT_PRECISION`. @@ -48,18 +77,71 @@ The default value of this variable is 100. This will be used as maximum precision for operations which may produce infinite digits (inverse, sqrt, ...). -Note that other operations, such as multiplication, will preserve all digits, so multiplying two 70 digit numbers -will result in one 140 digit number. -The user will have to manually trim the number of digits after calculations to reasonable amounts using the -`x.with_prec(30)` method. +Note that other operations, such as multiplication, will preserve all digits; +so multiplying two 70 digit numbers will result in one 140 digit number. +The user will have to manually trim the number of digits after calculations to +reasonable amounts using the `x.with_prec(30)` method. + +A new set of methods with explicit precision and rounding modes is being worked +on, but even after those are introduced the default precision will have to be +used as the implicit value. + +#### Rounding mode + +The default Context uses this value for rounding. +Valid values are the variants of the [RoundingMode] enum. + +Defaults to `HalfEven`. + +[RoundingMode]: https://docs.rs/bigdecimal/latest/bigdecimal/rounding/enum.RoundingMode.html + + +#### Exponential Format Threshold + +The maximum number of leading zeros after the decimal place before +the formatter uses exponential form (i.e. scientific notation). -A new set of methods with explicit precision and rounding modes is being worked on, but even after those -are introduced the default precision will have to be used as the implicit value. +There is currently no mechanism to change this during runtime. +If you know of a good solution for number formatting in Rust, please let me know! -## Improvements -Work is being done on this codebase again and there are many features -and improvements on the way. +#### Example Compile time configuration + +Given the program: + +```rust +fn main() { + let n = BigDecimal::from(700); + println!("1/{n} = {}", n.inverse()); +} +``` + +Compiling with different environment variables prints different results + +``` +$ export BIG_DECIMAL_DEFAULT_PRECISION=8 +$ cargo run +1/700 = 0.0014285714 + +$ export RUST_BIGDECIMAL_DEFAULT_PRECISION=5 +$ cargo run +1/700 = 0.0014286 + +$ export RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE=Down +$ cargo run +1/700 = 0.0014285 + +$ export RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD=2 +$ cargo run +1/700 = 1.4285E-3 +``` + +> [!NOTE] +> These are **compile time** environment variables, and the BigDecimal +> library is not configurable at **runtime** via environment variable, or +> any kind of global variables, by default. +> +> This is for flexibility and performance. ## About @@ -82,8 +164,3 @@ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - - -## Community - -Join the conversation on Zulip: https://bigdecimal-rs.zulipchat.com diff --git a/build.rs b/build.rs index 8e43e1a..0db0ca3 100644 --- a/build.rs +++ b/build.rs @@ -16,6 +16,7 @@ fn main() { let outdir: PathBuf = std::env::var_os("OUT_DIR").unwrap().into(); write_default_precision_file(&outdir); write_default_rounding_mode(&outdir); + write_exponential_format_threshold_file(&outdir); } @@ -46,3 +47,20 @@ fn write_default_rounding_mode(outdir: &Path) { std::fs::write(rust_file_path, rust_file_contents).unwrap(); } + +/// Create write_default_rounding_mode.rs, containing definition of constant EXPONENTIAL_FORMAT_THRESHOLD loaded in src/impl_fmt.rs +fn write_exponential_format_threshold_file(outdir: &Path) { + let env_var = env::var("RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD").unwrap_or_else(|_| "5".to_owned()); + println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD"); + + let rust_file_path = outdir.join("exponential_format_threshold.rs"); + + let value: u32 = env_var + .parse::() + .expect("$RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD must be an integer > 0") + .into(); + + let rust_file_contents = format!("const EXPONENTIAL_FORMAT_THRESHOLD: i64 = {};", value); + + std::fs::write(rust_file_path, rust_file_contents).unwrap(); +} diff --git a/examples/floating-precision.rs b/examples/floating-precision.rs index 2813a1e..14f4e8f 100644 --- a/examples/floating-precision.rs +++ b/examples/floating-precision.rs @@ -4,7 +4,7 @@ use bigdecimal::BigDecimal; use std::str::FromStr; fn main() { - let input = std::env::args().skip(1).next().unwrap_or("0.7".to_string()); + let input = std::env::args().nth(1).unwrap_or("0.7".to_string()); let decimal = BigDecimal::from_str(&input).expect("invalid decimal"); let floating = f32::from_str(&input).expect("invalid float"); diff --git a/examples/simple-math.rs b/examples/simple-math.rs index c4cab2d..097cde0 100644 --- a/examples/simple-math.rs +++ b/examples/simple-math.rs @@ -21,8 +21,8 @@ sum mut: 48.00000000000000 fn main() { println!("Hello, Big Decimals!"); let input = "0.8"; - let dec = BigDecimal::from_str(&input).unwrap(); - let float = f32::from_str(&input).unwrap(); + let dec = BigDecimal::from_str(input).unwrap(); + let float = f32::from_str(input).unwrap(); println!("Input ({}) with 10 decimals: {} vs {})", input, dec, float); let bd_square = dec.square(); diff --git a/scripts/bigdecimal-property-tests b/scripts/bigdecimal-property-tests index 8196fcb..7141277 100755 --- a/scripts/bigdecimal-property-tests +++ b/scripts/bigdecimal-property-tests @@ -5,12 +5,27 @@ # Tests are defined in src/lib.tests.property-test.rs # +SED=$(command -v gsed || command -v sed) +if [ -z "$SED" ]; then + echo "This program requires sed" + exit 1 +fi + +usage() { + echo "Usage: $0 [enable|disable|test|run ]" + echo "" + echo " enable: Enable property tests by removing comments in Cargo.toml & build.rs" + echo " disable: Restore property-tests comments in Cargo.toml & build.rs" + echo " test: Runs 'cargo test' between enabling & disabling property tests" + echo "run : Run user supplied command between enabling & disabling property tests" +} + enable_property_tests() { # enable property-test dependencies in Cargo - sed -i.bak -e 's|# PROPERTY-TESTS: ||' Cargo.toml + ${SED} -i.bak -e 's|# PROPERTY-TESTS: ||' Cargo.toml # add the property-test configuration in build.rs - sed -i.bak -e 's|// ::PROPERTY-TESTS:: ||' build.rs + ${SED} -i.bak -e 's|// ::PROPERTY-TESTS:: ||' build.rs } @@ -21,7 +36,7 @@ restore_disabled_property_tests() { } -DEFAULT_CMD=run +DEFAULT_CMD=help CMD=${1:-$DEFAULT_CMD} shift @@ -46,4 +61,8 @@ case "${CMD}" in disable) restore_disabled_property_tests ;; + + *) + usage + ;; esac diff --git a/scripts/make-fmt-format-tests.py b/scripts/make-fmt-format-tests.py new file mode 100755 index 0000000..8aa08fd --- /dev/null +++ b/scripts/make-fmt-format-tests.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# +# Generate Rust code given Decimals and formatting strings +# + +import sys +import textwrap +from decimal import * + +# Decimal strings to the testing formats +INPUTS = { + "1": [ + "{}", + "{:.1}", + "{:.4}", + "{:4.1}", + "{:>4.1}", + "{:<4.1}", + "{:+05.1}", + ], + "123456": [ + "{}", + "{:+05.1}", + "{:.1}", + "{:.4}", + "{:4.1}", + "{:>4.3}", + "{:>4.4}", + "{:<4.1}", + ], + "9999999": [ + "{:.4}", + "{:.8}", + ], + "19073.97235939614856": [ + "{}", + "{:+05.7}", + "{:.3}", + "{:0.4}", + "{:4.1}", + "{:>8.3}", + "{:>8.4}", + "{:<8.1}", + ], + "491326e-12": [ + "{}", + "{:+015.7}", + "{:.3}", + "{:0.4}", + "{:4.1}", + "{:>8.3}", + "{:>8.4}", + "{:<8.1}", + ], +} + + +def main(): + from argparse import ArgumentParser, FileType + + parser = ArgumentParser(description="Create test modules for src/impl_fmt.rs") + parser.add_argument("-o", "--output", nargs="?", default="-", type=FileType("w")) + args = parser.parse_args() + + for dec_str, formats in INPUTS.items(): + src = make_test_module_src(dec_str, formats) + args.output.write(src) + + return 0 + + +def make_test_module_src(dec_str: Decimal, formats: list[str]) -> str: + """ + Return Rust module source which tests given decimal aginst given formats + """ + dec = Decimal(dec_str) + mod_name = "dec_%s" % gen_name_from_dec(dec_str) + + formats_and_outputs = [ + (fmt, gen_name_from_fmt(fmt), fmt.format(dec)) for fmt in formats + ] + + max_len_name_and_fmt = max( + len(fmt) + len(name) for fmt, name, _ in formats_and_outputs + ) + + lines = [] + for fmt, name, value in formats_and_outputs: + spacer = " " * (max_len_name_and_fmt - (len(fmt) + len(name)) + 2) + lines.append(f'impl_case!(fmt_{name}:{spacer}"{fmt}" => "{value}");') + + body = textwrap.indent("\n".join(lines), " ") + text = textwrap.dedent( + f""" + mod {mod_name} {{ + use super::*; + + fn test_input() -> BigDecimal {{ + "{dec_str}".parse().unwrap() + }} + + %s + }} + """ + ) + return text % body + + +def gen_name_from_dec(dec_str: str) -> str: + """Given decimal input string return valid function/module name""" + return "".join(map_fmt_char_to_name_char(c) for c in dec_str) + + +def gen_name_from_fmt(fmt: str) -> str: + """Given decimal input string return valid function/module name""" + if fmt == "{}": + return "default" + else: + assert fmt.startswith("{:") and fmt.endswith("}") + return "".join(map_fmt_char_to_name_char(c) for c in fmt[2:-1]) + + +def map_fmt_char_to_name_char(c: str) -> str: + """Turn special characters into identifiers""" + match c: + case ".": + return "d" + case "+": + return "p" + case "-": + return "n" + case "<": + return "l" + case ">": + return "r" + case ":": + return "c" + case _: + return str(c) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/arithmetic/addition.rs b/src/arithmetic/addition.rs new file mode 100644 index 0000000..11f04b9 --- /dev/null +++ b/src/arithmetic/addition.rs @@ -0,0 +1,110 @@ +//! addition routines +//! + +use crate::*; + + +pub(crate) fn add_bigdecimals( + mut a: BigDecimal, + mut b: BigDecimal, +) -> BigDecimal { + if b.is_zero() { + if a.scale < b.scale { + a.int_val *= ten_to_the((b.scale - a.scale) as u64); + a.scale = b.scale; + } + return a; + } + + if a.is_zero() { + if b.scale < a.scale { + b.int_val *= ten_to_the((a.scale - b.scale) as u64); + b.scale = a.scale; + } + return b; + } + + let (a, b) = match a.scale.cmp(&b.scale) { + Ordering::Equal => (a, b), + Ordering::Less => (a.take_and_scale(b.scale), b), + Ordering::Greater => (b.take_and_scale(a.scale), a), + }; + + add_aligned_bigdecimals(a, b) +} + +fn add_aligned_bigdecimals( + mut a: BigDecimal, + mut b: BigDecimal, +) -> BigDecimal { + debug_assert_eq!(a.scale, b.scale); + if a.int_val.bits() >= b.int_val.bits() { + a.int_val += b.int_val; + a + } else { + b.int_val += a.int_val; + b + } +} + + +pub(crate) fn add_bigdecimal_refs<'a, 'b, Lhs, Rhs>( + lhs: Lhs, + rhs: Rhs, +) -> BigDecimal +where + Rhs: Into>, + Lhs: Into>, +{ + let lhs = lhs.into(); + let rhs = rhs.into(); + if rhs.is_zero() { + return lhs.to_owned(); + } + if lhs.is_zero() { + return rhs.to_owned(); + } + if lhs.scale >= rhs.scale { + lhs.to_owned() + rhs + } else { + rhs.to_owned() + lhs + } +} + + +pub(crate) fn addassign_bigdecimals( + lhs: &mut BigDecimal, + rhs: BigDecimal, +) { + if rhs.is_zero() { + return; + } + if lhs.is_zero() { + *lhs = rhs; + return; + } + lhs.add_assign(rhs.to_ref()); +} + + +pub(crate) fn addassign_bigdecimal_ref<'a, T: Into>>( + lhs: &mut BigDecimal, + rhs: T, +) { + // TODO: Replace to_owned() with efficient addition algorithm + let rhs = rhs.into().to_owned(); + match lhs.scale.cmp(&rhs.scale) { + Ordering::Less => { + let scaled = lhs.with_scale(rhs.scale); + lhs.int_val = scaled.int_val + &rhs.int_val; + lhs.scale = rhs.scale; + } + Ordering::Greater => { + let scaled = rhs.with_scale(lhs.scale); + lhs.int_val += scaled.int_val; + } + Ordering::Equal => { + lhs.int_val += &rhs.int_val; + } + } +} diff --git a/src/arithmetic/mod.rs b/src/arithmetic/mod.rs index 614503d..46644bc 100644 --- a/src/arithmetic/mod.rs +++ b/src/arithmetic/mod.rs @@ -2,6 +2,7 @@ use crate::*; +pub(crate) mod addition; pub(crate) mod sqrt; pub(crate) mod cbrt; pub(crate) mod inverse; @@ -14,6 +15,12 @@ pub(crate) fn ten_to_the(pow: u64) -> BigInt { ten_to_the_uint(pow).into() } +/// Return 10^{pow} as u64 +pub(crate) fn ten_to_the_u64(pow: u8) -> u64 { + debug_assert!(pow < 20); + 10u64.pow(pow as u32) +} + /// Return 10^pow pub(crate) fn ten_to_the_uint(pow: u64) -> BigUint { if pow < 20 { @@ -74,3 +81,30 @@ pub(crate) fn count_decimal_digits_uint(uint: &BigUint) -> u64 { } digits } + + +/// Return difference of two numbers, returning diff as u64 +pub(crate) fn diff(a: T, b: T) -> (Ordering, u64) +where + T: ToPrimitive + stdlib::ops::Sub + stdlib::cmp::Ord +{ + use stdlib::cmp::Ordering::*; + + match a.cmp(&b) { + Less => (Less, (b - a).to_u64().unwrap()), + Greater => (Greater, (a - b).to_u64().unwrap()), + Equal => (Equal, 0), + } +} + +/// Return difference of two numbers, returning diff as usize +#[allow(dead_code)] +pub(crate) fn diff_usize(a: usize, b: usize) -> (Ordering, usize) { + use stdlib::cmp::Ordering::*; + + match a.cmp(&b) { + Less => (Less, b - a), + Greater => (Greater, a - b), + Equal => (Equal, 0), + } +} diff --git a/src/context.rs b/src/context.rs index c48cc84..32beda9 100644 --- a/src/context.rs +++ b/src/context.rs @@ -128,9 +128,7 @@ impl Context { A: Into>, B: Into>, { - let a = a.into(); - let b = b.into(); - let sum = a.to_owned() + b.to_owned(); + let sum = a.into() + b.into(); *dest = sum.with_precision_round(self.precision, self.rounding) } } diff --git a/src/impl_fmt.rs b/src/impl_fmt.rs new file mode 100644 index 0000000..d4710f9 --- /dev/null +++ b/src/impl_fmt.rs @@ -0,0 +1,801 @@ +//! Implementation of std::fmt traits & other stringification functions +//! + +use crate::*; +use stdlib::fmt::Write; + + +// const EXPONENTIAL_FORMAT_THRESHOLD: i64 = ${RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD} or 5; +include!(concat!(env!("OUT_DIR"), "/exponential_format_threshold.rs")); + + +impl fmt::Display for BigDecimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + dynamically_format_decimal(self.to_ref(), f, EXPONENTIAL_FORMAT_THRESHOLD) + } +} + +impl fmt::Display for BigDecimalRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + dynamically_format_decimal(*self, f, EXPONENTIAL_FORMAT_THRESHOLD) + } +} + + +impl fmt::LowerExp for BigDecimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerExp::fmt(&self.to_ref(), f) + } +} + +impl fmt::LowerExp for BigDecimalRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let abs_int = self.digits.to_str_radix(10); + format_exponential(*self, f, abs_int, "e") + } +} + + +impl fmt::UpperExp for BigDecimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::UpperExp::fmt(&self.to_ref(), f) + } +} + +impl fmt::UpperExp for BigDecimalRef<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let abs_int = self.digits.to_str_radix(10); + format_exponential(*self, f, abs_int, "E") + } +} + + +impl fmt::Debug for BigDecimal { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "BigDecimal(\"{}e{:}\")", self.int_val, -self.scale) + } else { + write!(f, + "BigDecimal(sign={:?}, scale={}, digits={:?})", + self.sign(), self.scale, self.int_val.magnitude().to_u64_digits() + ) + } + } +} + + +fn dynamically_format_decimal( + this: BigDecimalRef, + f: &mut fmt::Formatter, + threshold: i64, +) -> fmt::Result { + // Acquire the absolute integer as a decimal string + let abs_int = this.digits.to_str_radix(10); + + // use exponential form if decimal point is not "within" the number. + // "threshold" is max number of leading zeros before being considered + // "outside" the number + if this.scale < 0 || (this.scale > abs_int.len() as i64 + threshold) { + format_exponential(this, f, abs_int, "E") + } else { + format_full_scale(this, f, abs_int) + } +} + + +fn format_full_scale( + this: BigDecimalRef, + f: &mut fmt::Formatter, + abs_int: String, +) -> fmt::Result { + use stdlib::cmp::Ordering::*; + + let mut digits = abs_int.into_bytes(); + let mut exp = (this.scale as i128).neg(); + let non_negative = matches!(this.sign, Sign::Plus | Sign::NoSign); + + debug_assert_ne!(digits.len(), 0); + + match f.precision() { + // precision limits the number of digits - we have to round + Some(prec) if prec < digits.len() && 1 < digits.len() => { + apply_rounding_to_ascii_digits(&mut digits, &mut exp, prec, this.sign); + debug_assert_eq!(digits.len(), prec); + }, + _ => { + // not limited by precision + } + }; + + // add the decimal point to 'digits' buffer + match exp.cmp(&0) { + // do not add decimal point for "full" integer + Equal => { + } + + // never decimal point if only one digit long + Greater if digits.len() == 1 => { + } + + // we format with scientific notation if exponent is positive + Greater => { + debug_assert!(digits.len() > 1); + + // increase exp by len(digits)-1 (eg [ddddd]E+{exp} => [d.dddd]E+{exp+4}) + exp += digits.len() as i128 - 1; + + // push decimal point and rotate it to index '1' + digits.push(b'.'); + digits[1..].rotate_right(1); + } + + // decimal point is within the digits (ddd.ddddddd) + Less if (-exp as usize) < digits.len() => { + let digits_to_shift = digits.len() - exp.abs() as usize; + digits.push(b'.'); + digits[digits_to_shift..].rotate_right(1); + + // exp = 0 means exponential-part will be ignored in output + exp = 0; + } + + // decimal point is to the left of digits (0.0000dddddddd) + Less => { + let digits_to_shift = exp.abs() as usize - digits.len(); + + digits.push(b'0'); + digits.push(b'.'); + digits.extend(stdlib::iter::repeat(b'0').take(digits_to_shift)); + digits.rotate_right(digits_to_shift + 2); + + exp = 0; + } + } + + // move digits back into String form + let mut buf = String::from_utf8(digits).unwrap(); + + // add exp part to buffer (if not zero) + if exp != 0 { + write!(buf, "E{:+}", exp)?; + } + + // write buffer to formatter + f.pad_integral(non_negative, "", &buf) +} + + +fn format_exponential( + this: BigDecimalRef, + f: &mut fmt::Formatter, + abs_int: String, + e_symbol: &str, +) -> fmt::Result { + // Steps: + // 1. Truncate integer based on precision + // 2. calculate exponent from the scale and the length of the internal integer + // 3. Place decimal point after a single digit of the number, or omit if there is only a single digit + // 4. Append `E{exponent}` and format the resulting string based on some `Formatter` flags + + let mut exp = (this.scale as i128).neg(); + let mut digits = abs_int.into_bytes(); + + if digits.len() > 1 { + // only modify for precision if there is more than 1 decimal digit + if let Some(prec) = f.precision() { + apply_rounding_to_ascii_digits(&mut digits, &mut exp, prec, this.sign); + } + } + + let mut abs_int = String::from_utf8(digits).unwrap(); + + // Determine the exponent value based on the scale + // + // # First case: the integer representation falls completely behind the + // decimal point. + // + // Example of this.scale > abs_int.len(): + // 0.000001234509876 + // abs_int.len() = 10 + // scale = 15 + // target is 1.234509876 + // exponent = -6 + // + // Example of this.scale == abs_int.len(): + // 0.333333333333333314829616256247390992939472198486328125 + // abs_int.len() = 54 + // scale = 54 + // target is 3.33333333333333314829616256247390992939472198486328125 + // exponent = -1 + // + // # Second case: the integer representation falls around, or before the + // decimal point + // + // ## Case 2.1, entirely before the decimal point. + // Example of (abs_int.len() - this.scale) > abs_int.len(): + // 123450987600000 + // abs_int.len() = 10 + // scale = -5 + // location = 15 + // target is 1.234509876 + // exponent = 14 + // + // ## Case 2.2, somewhere around the decimal point. + // Example of (abs_int.len() - this.scale) < abs_int.len(): + // 12.339999999999999857891452847979962825775146484375 + // abs_int.len() = 50 + // scale = 48 + // target is 1.2339999999999999857891452847979962825775146484375 + // exponent = 1 + // + // For the (abs_int.len() - this.scale) == abs_int.len() I couldn't + // come up with an example + let exponent = abs_int.len() as i128 + exp as i128 - 1; + + if abs_int.len() > 1 { + // only add decimal point if there is more than 1 decimal digit + abs_int.insert(1, '.'); + } + + if exponent != 0 { + write!(abs_int, "{}{:+}", e_symbol, exponent)?; + } + + let non_negative = matches!(this.sign(), Sign::Plus | Sign::NoSign); + //pad_integral does the right thing although we have a decimal + f.pad_integral(non_negative, "", &abs_int) +} + + +#[inline(never)] +pub(crate) fn write_scientific_notation(n: &BigDecimal, w: &mut W) -> fmt::Result { + if n.is_zero() { + return w.write_str("0e0"); + } + + if n.int_val.sign() == Sign::Minus { + w.write_str("-")?; + } + + let digits = n.int_val.magnitude(); + + let dec_str = digits.to_str_radix(10); + let (first_digit, remaining_digits) = dec_str.as_str().split_at(1); + w.write_str(first_digit)?; + if !remaining_digits.is_empty() { + w.write_str(".")?; + w.write_str(remaining_digits)?; + } + write!(w, "e{}", remaining_digits.len() as i128 - n.scale as i128) +} + + +#[inline(never)] +pub(crate) fn write_engineering_notation(n: &BigDecimal, out: &mut W) -> fmt::Result { + if n.is_zero() { + return out.write_str("0e0"); + } + + if n.int_val.sign() == Sign::Minus { + out.write_char('-')?; + } + + let digits = n.int_val.magnitude(); + + let dec_str = digits.to_str_radix(10); + let digit_count = dec_str.len(); + + let top_digit_exponent = digit_count as i128 - n.scale as i128; + + let shift_amount = match top_digit_exponent.rem_euclid(3) { + 0 => 3, + i => i as usize, + }; + + let exp = top_digit_exponent - shift_amount as i128; + + // handle adding zero padding + if let Some(padding_zero_count) = shift_amount.checked_sub(dec_str.len()) { + let zeros = &"000"[..padding_zero_count]; + out.write_str(&dec_str)?; + out.write_str(zeros)?; + return write!(out, "e{}", exp); + } + + let (head, rest) = dec_str.split_at(shift_amount); + debug_assert_eq!(exp % 3, 0); + + out.write_str(head)?; + + if !rest.is_empty() { + out.write_char('.')?; + out.write_str(rest)?; + } + + return write!(out, "e{}", exp); +} + + +/// Round big-endian digits in ascii +fn apply_rounding_to_ascii_digits( + ascii_digits: &mut Vec, + exp: &mut i128, + prec: usize, + sign: Sign +) { + if ascii_digits.len() < prec { + return; + } + + // shift exp to align with new length of digits + *exp += (ascii_digits.len() - prec) as i128; + + // true if all ascii_digits after precision are zeros + let trailing_zeros = ascii_digits[prec + 1..].iter().all(|&d| d == b'0'); + + let sig_digit = ascii_digits[prec - 1] - b'0'; + let insig_digit = ascii_digits[prec] - b'0'; + let rounded_digit = Context::default().round_pair(sign, sig_digit, insig_digit, trailing_zeros); + + // remove insignificant digits + ascii_digits.truncate(prec - 1); + + // push rounded value + if rounded_digit < 10 { + ascii_digits.push(rounded_digit + b'0'); + return + } + + debug_assert_eq!(rounded_digit, 10); + + // push zero and carry-the-one + ascii_digits.push(b'0'); + + // loop through digits in reverse order (skip the 0 we just pushed) + let digits = ascii_digits.iter_mut().rev().skip(1); + for digit in digits { + if *digit < b'9' { + // we've carried the one as far as it will go + *digit += 1; + return; + } + + debug_assert_eq!(*digit, b'9'); + + // digit was a 9, set to zero and carry the one + // to the next digit + *digit = b'0'; + } + + // at this point all digits have become zero + // just set significant digit to 1 and increase exponent + // + // eg: 9999e2 ~> 0000e2 ~> 1000e3 + // + ascii_digits[0] = b'1'; + *exp += 1; +} + + +#[cfg(test)] +mod test { + use super::*; + use paste::*; + + /// test case builder for mapping decimal-string to formatted-string + /// define test_fmt_function! macro to test your function + #[cfg(test)] + macro_rules! impl_case { + ($name:ident : $in:literal => $ex:literal) => { + #[test] + fn $name() { + let n: BigDecimal = $in.parse().unwrap(); + let s = test_fmt_function!(n); + assert_eq!(&s, $ex); + } + }; + } + + /// "Mock" Formatter + /// + /// Given callable, forwards formatter to callable. + /// Required work-around due to lack of constructor in fmt::Formatter + /// + struct Fmt(F) + where F: Fn(&mut fmt::Formatter) -> fmt::Result; + + impl fmt::Display for Fmt + where F: Fn(&mut fmt::Formatter) -> fmt::Result + { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // call closure with given formatter + (self.0)(f) + } + } + + impl fmt::Debug for Fmt + where F: Fn(&mut fmt::Formatter) -> fmt::Result + { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + (self.0)(f) + } + } + + mod dynamic_fmt { + use super::*; + + macro_rules! test_fmt_function { + ($n:ident) => {{ + format!("{}", Fmt(|f| dynamically_format_decimal($n.to_ref(), f, 2))) + }}; + } + + impl_case!(case_0d123: "0.123" => "0.123"); + impl_case!(case_0d0123: "0.0123" => "0.0123"); + impl_case!(case_0d00123: "0.00123" => "0.00123"); + impl_case!(case_0d000123: "0.000123" => "1.23E-4"); + + impl_case!(case_123d: "123." => "123"); + impl_case!(case_123de1: "123.e1" => "1.23E+3"); + } + + mod fmt_options { + use super::*; + + macro_rules! impl_case { + ($name:ident: $fmt:literal => $expected:literal) => { + #[test] + fn $name() { + let x = test_input(); + let y = format!($fmt, x); + assert_eq!(y, $expected); + } + }; + } + + mod dec_1 { + use super::*; + + fn test_input() -> BigDecimal { + "1".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "1"); + impl_case!(fmt_d1: "{:.1}" => "1"); + impl_case!(fmt_d4: "{:.4}" => "1"); + impl_case!(fmt_4d1: "{:4.1}" => " 1"); + impl_case!(fmt_r4d1: "{:>4.1}" => " 1"); + impl_case!(fmt_l4d1: "{:<4.1}" => "1 "); + impl_case!(fmt_p05d1: "{:+05.1}" => "+0001"); + } + + mod dec_123456 { + use super::*; + + fn test_input() -> BigDecimal { + "123456".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "123456"); + impl_case!(fmt_p05d1: "{:+05.1}" => "+1E+5"); + impl_case!(fmt_d1: "{:.1}" => "1E+5"); + impl_case!(fmt_d4: "{:.4}" => "1.235E+5"); + impl_case!(fmt_4d1: "{:4.1}" => "1E+5"); + impl_case!(fmt_r4d3: "{:>4.3}" => "1.23E+5"); + impl_case!(fmt_r4d4: "{:>4.4}" => "1.235E+5"); + impl_case!(fmt_l4d1: "{:<4.1}" => "1E+5"); + } + + mod dec_9999999 { + use super::*; + + fn test_input() -> BigDecimal { + "9999999".parse().unwrap() + } + + impl_case!(fmt_d4: "{:.4}" => "1.000E+7"); + impl_case!(fmt_d8: "{:.8}" => "9999999"); + } + + mod dec_19073d97235939614856 { + use super::*; + + fn test_input() -> BigDecimal { + "19073.97235939614856".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "19073.97235939614856"); + impl_case!(fmt_p05d7: "{:+05.7}" => "+19073.97"); + impl_case!(fmt_d3: "{:.3}" => "1.91E+4"); + impl_case!(fmt_0d4: "{:0.4}" => "1.907E+4"); + impl_case!(fmt_4d1: "{:4.1}" => "2E+4"); + impl_case!(fmt_r8d3: "{:>8.3}" => " 1.91E+4"); + impl_case!(fmt_r8d4: "{:>8.4}" => "1.907E+4"); + impl_case!(fmt_l8d1: "{:<8.1}" => "2E+4 "); + } + + mod dec_491326en12 { + use super::*; + + fn test_input() -> BigDecimal { + "491326e-12".parse().unwrap() + } + + impl_case!(fmt_default: "{}" => "4.91326E-7"); + impl_case!(fmt_p015d7: "{:+015.7}" => "+00004.91326E-7"); + impl_case!(fmt_d3: "{:.3}" => "4.91E-7"); + impl_case!(fmt_0d4: "{:0.4}" => "4.913E-7"); + impl_case!(fmt_4d1: "{:4.1}" => "5E-7"); + impl_case!(fmt_r8d3: "{:>8.3}" => " 4.91E-7"); + impl_case!(fmt_r8d4: "{:>8.4}" => "4.913E-7"); + impl_case!(fmt_l8d1: "{:<8.1}" => "5E-7 "); + } + } + + mod fmt_boundaries { + use super::*; + + macro_rules! impl_case { + ( $name:ident: $src:expr => $expected:literal ) => { + #[test] + fn $name() { + let src = $src; + let bd: BigDecimal = src.parse().unwrap(); + let result = bd.to_string(); + assert_eq!(result, $expected); + + bd.to_scientific_notation(); + bd.to_engineering_notation(); + + let round_trip = BigDecimal::from_str(&result).unwrap(); + assert_eq!(round_trip, bd); + } + }; + ( (panics) $name:ident: $src:expr ) => { + #[test] + #[should_panic] + fn $name() { + let src = $src; + let _bd: BigDecimal = src.parse().unwrap(); + } + }; + } + + impl_case!(test_max: format!("1E{}", i64::MAX) => "1E+9223372036854775807"); + impl_case!(test_max_multiple_digits: format!("314156E{}", i64::MAX) => "3.14156E+9223372036854775812"); + impl_case!(test_min_scale: "1E9223372036854775808" => "1E+9223372036854775808"); + impl_case!(test_max_scale: "1E-9223372036854775807" => "1E-9223372036854775807"); + impl_case!(test_min_multiple_digits: format!("271828182E-{}", i64::MAX) => "2.71828182E-9223372036854775799"); + + impl_case!((panics) test_max_exp_overflow: "1E9223372036854775809"); + impl_case!((panics) test_min_exp_overflow: "1E-9223372036854775808"); + } + + #[test] + fn test_fmt() { + let vals = vec![ + // b s ( {} {:.1} {:.4} {:4.1} {:+05.7} {:<6.4} + (1, 0, ( "1", "1", "1", " 1", "+0001", "1 " )), + (1, 1, ( "0.1", "0.1", "0.1", " 0.1", "+00.1", "0.1 " )), + (1, 2, ( "0.01", "0.01", "0.01", "0.01", "+0.01", "0.01 " )), + (1, -2, ( "1E+2", "1E+2", "1E+2", "1E+2", "+1E+2", "1E+2 " )), + (-1, 0, ( "-1", "-1", "-1", " -1", "-0001", "-1 " )), + (-1, 1, ( "-0.1", "-0.1", "-0.1", "-0.1", "-00.1", "-0.1 " )), + (-1, 2, ("-0.01", "-0.01", "-0.01", "-0.01", "-0.01", "-0.01 " )), + ]; + for (i, scale, results) in vals { + let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); + assert_eq!(format!("{}", x), results.0); + assert_eq!(format!("{:.1}", x), results.1); + assert_eq!(format!("{:.4}", x), results.2); + assert_eq!(format!("{:4.1}", x), results.3); + assert_eq!(format!("{:+05.7}", x), results.4); + assert_eq!(format!("{:<6.4}", x), results.5); + } + } + + #[test] + fn test_fmt_with_large_values() { + let vals = vec![ + // b s ( {} {:.1} {:2.4} {:4.2} {:+05.7} {:<13.4} + // Numbers with large scales + (1, 10_000, ( "1E-10000", "1E-10000", "1E-10000", "1E-10000", "+1E-10000", "1E-10000 ")), + (1, -10_000, ( "1E+10000", "1E+10000", "1E+10000", "1E+10000", "+1E+10000", "1E+10000 ")), + // // Numbers with many digits + (1234506789, 5, ( "12345.06789", "1E+4", "1.235E+4", "1.2E+4", "+12345.07", "1.235E+4 ")), + (1234506789, -5, ( "1.234506789E+14", "1E+14", "1.235E+14", "1.2E+14", "+1.234507E+14", "1.235E+14 ")), + ]; + for (i, scale, results) in vals { + let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); + assert_eq!(format!("{}", x), results.0, "digits={} scale={}", i, scale); + assert_eq!(format!("{:.1}", x), results.1, "digits={} scale={}", i, scale); + assert_eq!(format!("{:2.4}", x), results.2, "digits={} scale={}", i, scale); + assert_eq!(format!("{:4.2}", x), results.3, "digits={} scale={}", i, scale); + assert_eq!(format!("{:+05.7}", x), results.4, "digits={} scale={}", i, scale); + assert_eq!(format!("{:<13.4}", x), results.5, "digits={} scale={}", i, scale); + } + } + + mod fmt_debug { + use super::*; + + macro_rules! impl_case { + ($name:ident: $input:literal => $expected:literal => $expected_alt:literal) => { + paste! { + #[test] + fn $name() { + let x: BigDecimal = $input.parse().unwrap(); + let y = format!("{:?}", x); + assert_eq!(y, $expected); + } + + #[test] + fn [< $name _alt >]() { + let x: BigDecimal = $input.parse().unwrap(); + let y = format!("{:#?}", x); + assert_eq!(y, $expected_alt); + } + } + } + } + + impl_case!(case_0: "0" => r#"BigDecimal(sign=NoSign, scale=0, digits=[])"# + => r#"BigDecimal("0e0")"#); + + impl_case!(case_n0: "-0" => r#"BigDecimal(sign=NoSign, scale=0, digits=[])"# + => r#"BigDecimal("0e0")"#); + + impl_case!(case_1: "1" => r#"BigDecimal(sign=Plus, scale=0, digits=[1])"# + => r#"BigDecimal("1e0")"#); + + impl_case!(case_123_400: "123.400" => r#"BigDecimal(sign=Plus, scale=3, digits=[123400])"# + => r#"BigDecimal("123400e-3")"#); + + impl_case!(case_123_4en2: "123.4e-2" => r#"BigDecimal(sign=Plus, scale=3, digits=[1234])"# + => r#"BigDecimal("1234e-3")"#); + + impl_case!(case_123_456: "123.456" => r#"BigDecimal(sign=Plus, scale=3, digits=[123456])"# + => r#"BigDecimal("123456e-3")"#); + + impl_case!(case_01_20: "01.20" => r#"BigDecimal(sign=Plus, scale=2, digits=[120])"# + => r#"BigDecimal("120e-2")"#); + + impl_case!(case_1_20: "1.20" => r#"BigDecimal(sign=Plus, scale=2, digits=[120])"# + => r#"BigDecimal("120e-2")"#); + impl_case!(case_01_2e3: "01.2E3" => r#"BigDecimal(sign=Plus, scale=-2, digits=[12])"# + => r#"BigDecimal("12e2")"#); + + impl_case!(case_avagadro: "6.02214076e1023" => r#"BigDecimal(sign=Plus, scale=-1015, digits=[602214076])"# + => r#"BigDecimal("602214076e1015")"#); + + impl_case!(case_1e99999999999999 : "1e99999999999999" => r#"BigDecimal(sign=Plus, scale=-99999999999999, digits=[1])"# + => r#"BigDecimal("1e99999999999999")"#); + + impl_case!(case_n144d3308279 : "-144.3308279" => r#"BigDecimal(sign=Minus, scale=7, digits=[1443308279])"# + => r#"BigDecimal("-1443308279e-7")"#); + + impl_case!(case_n349983058835858339619e2 : "-349983058835858339619e2" + => r#"BigDecimal(sign=Minus, scale=-2, digits=[17941665509086410531, 18])"# + => r#"BigDecimal("-349983058835858339619e2")"#); + } + + mod write_scientific_notation { + use super::*; + + macro_rules! test_fmt_function { + ($n:expr) => { $n.to_scientific_notation() }; + } + + impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); + impl_case!(case_1_234e_5 : "0.00001234" => "1.234e-5"); + impl_case!(case_0 : "0" => "0e0"); + impl_case!(case_1 : "1" => "1e0"); + impl_case!(case_2_00e0 : "2.00" => "2.00e0"); + impl_case!(case_neg_5_70e1 : "-57.0" => "-5.70e1"); + } + + mod write_engineering_notation { + use super::*; + + macro_rules! test_fmt_function { + ($n:expr) => { $n.to_engineering_notation() }; + } + + impl_case!(case_4_1592480782835e9 : "4159248078.2835" => "4.1592480782835e9"); + impl_case!(case_12_34e_6 : "0.00001234" => "12.34e-6"); + impl_case!(case_0 : "0" => "0e0"); + impl_case!(case_1 : "1" => "1e0"); + impl_case!(case_2_00e0 : "2.00" => "2.00e0"); + impl_case!(case_neg_5_70e1 : "-57.0" => "-57.0e0"); + impl_case!(case_5_31e4 : "5.31e4" => "53.1e3"); + impl_case!(case_5_31e5 : "5.31e5" => "531e3"); + impl_case!(case_5_31e6 : "5.31e6" => "5.31e6"); + impl_case!(case_5_31e7 : "5.31e7" => "53.1e6"); + + impl_case!(case_1e2 : "1e2" => "100e0"); + impl_case!(case_1e119 : "1e19" => "10e18"); + impl_case!(case_1e3000 : "1e3000" => "1e3000"); + impl_case!(case_4_2e7 : "4.2e7" => "42e6"); + impl_case!(case_4_2e8 : "4.2e8" => "420e6"); + + impl_case!(case_4e99999999999999 : "4e99999999999999" => "4e99999999999999"); + impl_case!(case_4e99999999999998 : "4e99999999999998" => "400e99999999999996"); + impl_case!(case_44e99999999999998 : "44e99999999999998" => "4.4e99999999999999"); + impl_case!(case_4e99999999999997 : "4e99999999999997" => "40e99999999999996"); + impl_case!(case_41e99999999999997 : "41e99999999999997" => "410e99999999999996"); + impl_case!(case_413e99999999999997 : "413e99999999999997" => "4.13e99999999999999"); + // impl_case!(case_413e99999999999997 : "413e99999999999997" => "4.13e99999999999999"); + } +} + + +#[cfg(all(test, property_tests))] +mod proptests { + use super::*; + use paste::paste; + use proptest::prelude::*; + + macro_rules! impl_parsing_test { + ($t:ty) => { + paste! { proptest! { + #[test] + fn [< roudtrip_to_str_and_back_ $t >](n: $t) { + let original = BigDecimal::from(n); + let display = format!("{}", original); + let parsed = display.parse::().unwrap(); + + prop_assert_eq!(&original, &parsed); + } + } } + }; + (from-float $t:ty) => { + paste! { proptest! { + #[test] + fn [< roudtrip_to_str_and_back_ $t >](n: $t) { + let original = BigDecimal::try_from(n).unwrap(); + let display = format!("{}", original); + let parsed = display.parse::().unwrap(); + + prop_assert_eq!(&original, &parsed); + } + } } + }; + } + + impl_parsing_test!(u8); + impl_parsing_test!(u16); + impl_parsing_test!(u32); + impl_parsing_test!(u64); + impl_parsing_test!(u128); + + impl_parsing_test!(i8); + impl_parsing_test!(i16); + impl_parsing_test!(i32); + impl_parsing_test!(i64); + impl_parsing_test!(i128); + + impl_parsing_test!(from-float f32); + impl_parsing_test!(from-float f64); + + proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn scientific_notation_roundtrip(f: f64) { + prop_assume!(!f.is_nan() && !f.is_infinite()); + let n = BigDecimal::from_f64(f).unwrap(); + let s = n.to_scientific_notation(); + let m: BigDecimal = s.parse().unwrap(); + prop_assert_eq!(n, m); + } + + #[test] + fn engineering_notation_roundtrip(f: f64) { + prop_assume!(!f.is_nan() && !f.is_infinite()); + let n = BigDecimal::from_f64(f).unwrap(); + let s = n.to_engineering_notation(); + let m: BigDecimal = s.parse().unwrap(); + prop_assert_eq!(n, m); + } + } +} diff --git a/src/impl_num.rs b/src/impl_num.rs index 8136973..32d1e5d 100644 --- a/src/impl_num.rs +++ b/src/impl_num.rs @@ -44,16 +44,8 @@ impl Num for BigDecimal { // 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)?) + let (base, e_exp) = s.split_at(loc); + (base, i128::from_str(&e_exp[1..])?) } }; @@ -62,39 +54,49 @@ impl Num for BigDecimal { return Err(ParseBigDecimalError::Empty); } + let mut digit_buffer = String::new(); + + let last_digit_loc = base_part.len() - 1; + // split decimal into a digit string and decimal-point offset - let (digits, decimal_offset): (String, _) = match base_part.find('.') { + let (digits, decimal_offset) = match base_part.find('.') { // No dot! pass directly to BigInt - None => (base_part.to_string(), 0), - + None => (base_part, 0), + // dot at last digit, pass all preceding digits to BigInt + Some(loc) if loc == last_digit_loc => { + (&base_part[..last_digit_loc], 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..]); + digit_buffer.reserve(lead.len() + trail.len()); // copy all leading characters into 'digits' string - let mut digits = String::from(lead); - + digit_buffer.push_str(lead); // copy all trailing characters after '.' into the digits string - digits.push_str(trail); + digit_buffer.push_str(trail); // count number of trailing digits let trail_digits = trail.chars().filter(|c| *c != '_').count(); - (digits, trail_digits as i64) - } - }; - - let scale = match decimal_offset.checked_sub(exponent_value) { - Some(scale) => scale, - None => { - return Err(ParseBigDecimalError::Other( - format!("Exponent overflow when parsing '{}'", s) - )) + (digit_buffer.as_str(), trail_digits as i128) } }; - let big_int = BigInt::from_str_radix(&digits, radix)?; + // Calculate scale by subtracing the parsed exponential + // value from the number of decimal digits. + // Return error if anything overflows outside i64 boundary. + let scale = decimal_offset + .checked_sub(exponent_value) + .map(|scale| scale.to_i64()) + .flatten() + .ok_or_else(|| + ParseBigDecimalError::Other( + format!("Exponent overflow when parsing '{}'", s)) + )?; + + let big_int = BigInt::from_str_radix(digits, radix)?; Ok(BigDecimal::new(big_int, scale)) } diff --git a/src/impl_ops.rs b/src/impl_ops.rs index 565d94b..b7afec1 100644 --- a/src/impl_ops.rs +++ b/src/impl_ops.rs @@ -356,6 +356,7 @@ macro_rules! impl_div_for_primitive { impl Div<$t> for BigDecimal { type Output = BigDecimal; + #[allow(clippy::float_cmp)] fn div(self, denom: $t) -> BigDecimal { if !denom.is_normal() { BigDecimal::zero() diff --git a/src/impl_ops_add.rs b/src/impl_ops_add.rs index 8b80bfd..f056c95 100644 --- a/src/impl_ops_add.rs +++ b/src/impl_ops_add.rs @@ -10,22 +10,7 @@ impl Add for BigDecimal { #[inline] fn add(self, rhs: BigDecimal) -> BigDecimal { - if rhs.is_zero() { - return self; - } - if self.is_zero() { - return rhs; - } - - let mut lhs = self; - match lhs.scale.cmp(&rhs.scale) { - Ordering::Equal => { - lhs.int_val += rhs.int_val; - lhs - } - Ordering::Less => lhs.take_and_scale(rhs.scale) + rhs, - Ordering::Greater => rhs.take_and_scale(lhs.scale) + lhs, - } + arithmetic::addition::add_bigdecimals(self, rhs) } } @@ -61,18 +46,7 @@ impl Add for &'_ BigDecimal { impl<'a, T: Into>> Add for &'_ BigDecimal { type Output = BigDecimal; fn add(self, rhs: T) -> BigDecimal { - let rhs = rhs.into(); - if rhs.is_zero() { - return self.clone(); - } - if self.is_zero() { - return rhs.to_owned(); - } - if self.scale >= rhs.scale { - self.to_owned() + rhs - } else { - rhs.to_owned() + self - } + arithmetic::addition::add_bigdecimal_refs(self, rhs) } } @@ -98,12 +72,7 @@ impl Add for BigDecimalRef<'_> { impl<'a, T: Into>> Add for BigDecimalRef<'_> { type Output = BigDecimal; fn add(self, rhs: T) -> BigDecimal { - let rhs = rhs.into(); - if self.scale >= rhs.scale { - self.to_owned() + rhs - } else { - rhs.to_owned() + self - } + arithmetic::addition::add_bigdecimal_refs(self, rhs) } } @@ -122,7 +91,7 @@ impl Add for BigInt { #[inline] fn add(self, rhs: BigDecimal) -> BigDecimal { - rhs + self + BigDecimal::from(self) + rhs } } @@ -130,7 +99,7 @@ impl<'a> Add<&'a BigDecimal> for BigInt { type Output = BigDecimal; fn add(self, rhs: &BigDecimal) -> BigDecimal { - rhs.to_ref().add(self) + BigDecimal::from(self) + rhs } } @@ -138,7 +107,7 @@ impl<'a> Add> for BigInt { type Output = BigDecimal; fn add(self, rhs: BigDecimalRef<'_>) -> BigDecimal { - rhs.add(self) + BigDecimal::from(self) + rhs } } @@ -173,43 +142,21 @@ impl<'a> Add> for &BigInt { impl AddAssign for BigDecimal { fn add_assign(&mut self, rhs: BigDecimal) { - if rhs.is_zero() { - return; - } - if self.is_zero() { - *self = rhs; - return; - } - self.add_assign(rhs.to_ref()); + arithmetic::addition::addassign_bigdecimals(self, rhs) } } impl<'a, N: Into>> AddAssign for BigDecimal { #[inline] fn add_assign(&mut self, rhs: N) { - // TODO: Replace to_owned() with efficient addition algorithm - let rhs = rhs.into().to_owned(); - match self.scale.cmp(&rhs.scale) { - Ordering::Less => { - let scaled = self.with_scale(rhs.scale); - self.int_val = scaled.int_val + &rhs.int_val; - self.scale = rhs.scale; - } - Ordering::Greater => { - let scaled = rhs.with_scale(self.scale); - self.int_val += scaled.int_val; - } - Ordering::Equal => { - self.int_val += &rhs.int_val; - } - } + arithmetic::addition::addassign_bigdecimal_ref(self, rhs) } } impl AddAssign for BigDecimal { #[inline] fn add_assign(&mut self, rhs: BigInt) { - self.add_assign(&rhs); + self.add_assign(BigDecimal::from(rhs)); } } diff --git a/src/impl_ops_mul.rs b/src/impl_ops_mul.rs index 2b1df37..181ce9d 100644 --- a/src/impl_ops_mul.rs +++ b/src/impl_ops_mul.rs @@ -30,15 +30,15 @@ impl<'a> Mul<&'a BigDecimal> for BigDecimal { if self.is_one() { self.scale = rhs.scale; self.int_val.set_zero(); - self.int_val.add_assign(&rhs.int_val); - self - } else if rhs.is_one() { - self - } else { + self.int_val += &rhs.int_val; + } else if rhs.is_zero() { + self.scale = 0; + self.int_val.set_zero(); + } else if !self.is_zero() && !rhs.is_one() { self.scale += rhs.scale; - MulAssign::mul_assign(&mut self.int_val, &rhs.int_val); - self + self.int_val *= &rhs.int_val; } + self } } diff --git a/src/impl_serde.rs b/src/impl_serde.rs new file mode 100644 index 0000000..6ba601d --- /dev/null +++ b/src/impl_serde.rs @@ -0,0 +1,194 @@ +//! +//! Support for serde implementations +//! +use crate::*; +use serde::{de, ser}; + + +impl ser::Serialize for BigDecimal { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.collect_str(&self) + } +} + +/// Used by SerDe to construct a BigDecimal +struct BigDecimalVisitor; + +impl<'de> de::Visitor<'de> for BigDecimalVisitor { + type Value = BigDecimal; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a number or formatted decimal string") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + BigDecimal::from_str(value).map_err(|err| E::custom(format!("{}", err))) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(BigDecimal::from(value)) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(BigDecimal::from(value)) + } + + fn visit_f64(self, value: f64) -> Result + where + E: de::Error, + { + BigDecimal::try_from(value).map_err(|err| E::custom(format!("{}", err))) + } +} + +#[cfg(not(feature = "string-only"))] +impl<'de> de::Deserialize<'de> for BigDecimal { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_any(BigDecimalVisitor) + } +} + +#[cfg(feature = "string-only")] +impl<'de> de::Deserialize<'de> for BigDecimal { + fn deserialize(d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_str(BigDecimalVisitor) + } +} + +#[cfg(test)] +mod test { + use super::*; + use paste::paste; + + use serde_test::{ + Token, assert_tokens, assert_de_tokens, assert_de_tokens_error + }; + + mod serde_serialize_deserialize_str { + use super::*; + + macro_rules! impl_case { + ($name:ident : $input:literal => $output:literal) => { + #[test] + fn $name() { + let expected = Token::Str($output); + let decimal: BigDecimal = $input.parse().unwrap(); + assert_tokens(&decimal, &[expected]); + } + } + } + + impl_case!(case_1d0: "1.0" => "1.0"); + impl_case!(case_0d5: "0.5" => "0.5"); + impl_case!(case_50: "50" => "50"); + impl_case!(case_50000: "50000" => "50000"); + impl_case!(case_1en3: "1e-3" => "0.001"); + impl_case!(case_10e11: "10e11" => "1.0E+12"); + impl_case!(case_d25: ".25" => "0.25"); + impl_case!(case_12d34e1: "12.34e1" => "123.4"); + impl_case!(case_40d0010: "40.0010" => "40.0010"); + } + + #[cfg(not(feature = "string-only"))] + mod serde_deserialize_int { + use super::*; + + macro_rules! impl_case { + ( $( $ttype:ident ),+ : -$input:literal ) => { + $( paste! { impl_case!([< case_n $input _ $ttype:lower >] : $ttype : -$input); } )* + }; + ( $( $ttype:ident ),+ : $input:literal ) => { + $( paste! { impl_case!([< case_ $input _ $ttype:lower >] : $ttype : $input); } )* + }; + ($name:ident : $type:ident : $input:literal) => { + #[test] + fn $name() { + let expected = BigDecimal::from($input); + let token = Token::$type($input); + assert_de_tokens(&expected, &[token]); + } + }; + } + + impl_case!(I8, I16, I32, I64, U8, U16, U32, U64 : 0); + impl_case!(I8, I16, I32, I64, U8, U16, U32, U64 : 1); + impl_case!(I8, I16, I32, I64 : -1); + impl_case!(I64: -99999999999i64); + impl_case!(I64: -9_223_372_036_854_775_808i64); + } + + #[cfg(not(feature = "string-only"))] + mod serde_deserialize_float { + use super::*; + + macro_rules! impl_case { + ( $name:ident : $input:literal => $ttype:ident : $expected:literal ) => { + paste! { + #[test] + fn [< $name _ $ttype:lower >]() { + let expected: BigDecimal = $expected.parse().unwrap(); + let token = Token::$ttype($input); + assert_de_tokens(&expected, &[token]); + } + } + }; + ( $name:ident : $input:literal => $( $ttype:ident : $expected:literal )+ ) => { + $( impl_case!($name : $input => $ttype : $expected); )* + }; + ( $name:ident : $input:literal => $( $ttype:ident ),+ : $expected:literal ) => { + $( impl_case!($name : $input => $ttype : $expected); )* + }; + } + + impl_case!(case_1d0 : 1.0 => F32, F64 : "1"); + impl_case!(case_1d1 : 1.1 => F32 : "1.10000002384185791015625" + F64 : "1.100000000000000088817841970012523233890533447265625"); + + impl_case!(case_0d001834988943300: + 0.001834988943300 => F32 : "0.001834988943301141262054443359375" + F64 : "0.00183498894330000003084768511740776375518180429935455322265625"); + + impl_case!(case_n869651d9131236838: + -869651.9131236838 => F32 : "-869651.9375" + F64 : "-869651.91312368377111852169036865234375"); + + impl_case!(case_n1en20: + -1e-20 => F32 : "-9.999999682655225388967887463487205224055287544615566730499267578125E-21" + F64 : "-999999999999999945153271454209571651729503702787392447107715776066783064379706047475337982177734375e-119"); + } + + #[cfg(not(feature = "string-only"))] + mod serde_deserialize_nan { + use super::*; + + #[test] + fn case_f32() { + let tokens = [ Token::F32(f32::NAN) ]; + assert_de_tokens_error::(&tokens, "NAN"); + } + + #[test] + fn case_f64() { + let tokens = [ Token::F64(f64::NAN) ]; + assert_de_tokens_error::(&tokens, "NAN"); + } + } +} diff --git a/src/impl_trait_from_str.rs b/src/impl_trait_from_str.rs new file mode 100644 index 0000000..4dd760f --- /dev/null +++ b/src/impl_trait_from_str.rs @@ -0,0 +1,80 @@ +use crate::*; +use stdlib::str::FromStr; + +impl FromStr for BigDecimal { + type Err = ParseBigDecimalError; + + #[inline] + fn from_str(s: &str) -> Result { + // implemented in impl_num.rs + BigDecimal::from_str_radix(s, 10) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! impl_case { + ($name:ident: $input:literal => $int:literal E $exp:literal) => { + #[test] + fn $name() { + let dec = BigDecimal::from_str($input).unwrap(); + assert_eq!(dec.int_val, $int.into()); + assert_eq!(dec.scale, -$exp); + } + }; + } + + impl_case!(case_1331d107: "1331.107" => 1331107 E -3 ); + impl_case!(case_1d0: "1.0" => 10 E -1 ); + impl_case!(case_2e1: "2e1" => 2 E 1 ); + impl_case!(case_0d00123: "0.00123" => 123 E -5); + impl_case!(case_n123: "-123" => -123 E -0); + impl_case!(case_n1230: "-1230" => -1230 E -0); + impl_case!(case_12d3: "12.3" => 123 E -1); + impl_case!(case_123en1: "123e-1" => 123 E -1); + impl_case!(case_1d23ep1: "1.23e+1" => 123 E -1); + impl_case!(case_1d23ep3: "1.23E+3" => 123 E 1); + impl_case!(case_1d23en8: "1.23E-8" => 123 E -10); + impl_case!(case_n1d23en10: "-1.23E-10" => -123 E -12); + impl_case!(case_123_: "123_" => 123 E -0); + impl_case!(case_31_862_140d830_686_979: "31_862_140.830_686_979" => 31862140830686979i128 E -9); + impl_case!(case_n1_1d2_2: "-1_1.2_2" => -1122 E -2); + impl_case!(case_999d521_939: "999.521_939" => 999521939 E -6); + impl_case!(case_679d35_84_03en2: "679.35_84_03E-2" => 679358403 E -8); + impl_case!(case_271576662d_e4: "271576662.__E4" => 271576662 E 4); + + impl_case!(case_1_d_2: "1_._2" => 12 E -1); +} + + +#[cfg(test)] +mod test_invalid { + use super::*; + + macro_rules! impl_case { + ($name:ident: $input:literal => $exp:literal) => { + #[test] + #[should_panic(expected = $exp)] + fn $name() { + BigDecimal::from_str($input).unwrap(); + } + }; + } + + impl_case!(case_bad_string_empty : "" => "Empty"); + impl_case!(case_bad_string_empty_exponent : "123.123E" => "Empty"); + impl_case!(case_bad_string_only_decimal_point : "." => "Empty"); + impl_case!(test_bad_string_only_decimal_and_exponent : ".e4" => "Empty"); + + impl_case!(test_bad_string_only_decimal_and_underscore : "_._" => "InvalidDigit"); + + impl_case!(case_bad_string_hello : "hello" => "InvalidDigit"); + impl_case!(case_bad_string_nan : "nan" => "InvalidDigit"); + impl_case!(case_bad_string_invalid_char : "12z3.12" => "InvalidDigit"); + impl_case!(case_bad_string_nan_exponent : "123.123eg" => "InvalidDigit"); + impl_case!(case_bad_string_multiple_decimal_points : "123.12.45" => "InvalidDigit"); + impl_case!(case_bad_string_hex : "0xCafeBeef" => "InvalidDigit"); +} diff --git a/src/lib.rs b/src/lib.rs index e461a6a..c8b328c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,11 +41,14 @@ //! ``` #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::style)] +#![allow(clippy::excessive_precision)] #![allow(clippy::unreadable_literal)] #![allow(clippy::needless_return)] #![allow(clippy::suspicious_arithmetic_impl)] #![allow(clippy::suspicious_op_assign_impl)] #![allow(clippy::redundant_field_names)] +#![allow(clippy::approx_constant)] +#![cfg_attr(test, allow(clippy::useless_vec))] #![allow(unused_imports)] @@ -59,6 +62,9 @@ extern crate paste; #[cfg(feature = "serde")] extern crate serde; +#[cfg(all(test, feature = "serde"))] +extern crate serde_test; + #[cfg(feature = "std")] include!("./with_std.rs"); @@ -78,6 +84,7 @@ use self::stdlib::iter::Sum; use self::stdlib::str::FromStr; use self::stdlib::string::{String, ToString}; use self::stdlib::fmt; +use self::stdlib::Vec; use num_bigint::{BigInt, BigUint, ParseBigIntError, Sign}; use num_integer::Integer as IntegerTrait; @@ -97,6 +104,7 @@ mod arithmetic; // From, To, TryFrom impls mod impl_convert; +mod impl_trait_from_str; // Add, Sub, etc... mod impl_ops; @@ -112,6 +120,13 @@ mod impl_cmp; // Implementations of num_traits mod impl_num; +// Implementations of std::fmt traits and stringificaiton routines +mod impl_fmt; + +// Implementations for deserializations and serializations +#[cfg(feature = "serde")] +pub mod impl_serde; + // construct BigDecimals from strings and floats mod parsing; @@ -126,10 +141,14 @@ pub use context::Context; use arithmetic::{ ten_to_the, ten_to_the_uint, + ten_to_the_u64, + diff, + diff_usize, count_decimal_digits, count_decimal_digits_uint, }; + /// Internal function used for rounding /// /// returns 1 if most significant digit is >= 5, otherwise 0 @@ -365,20 +384,31 @@ impl BigDecimal { /// fn take_and_scale(mut self, new_scale: i64) -> BigDecimal { if self.int_val.is_zero() { - return BigDecimal::new(BigInt::zero(), new_scale); + self.scale = new_scale; + return self; } - match new_scale.cmp(&self.scale) { - Ordering::Greater => { - self.int_val *= ten_to_the((new_scale - self.scale) as u64); - BigDecimal::new(self.int_val, new_scale) + match diff(new_scale, self.scale) { + (Ordering::Greater, scale_diff) => { + self.scale = new_scale; + if scale_diff < 20 { + self.int_val *= ten_to_the_u64(scale_diff as u8); + } else { + self.int_val *= ten_to_the(scale_diff); + } } - Ordering::Less => { - self.int_val /= ten_to_the((self.scale - new_scale) as u64); - BigDecimal::new(self.int_val, new_scale) + (Ordering::Less, scale_diff) => { + self.scale = new_scale; + if scale_diff < 20 { + self.int_val /= ten_to_the_u64(scale_diff as u8); + } else { + self.int_val /= ten_to_the(scale_diff); + } } - Ordering::Equal => self, + (Ordering::Equal, _) => {}, } + + self } /// Take and return bigdecimal with the given sign @@ -868,9 +898,53 @@ impl BigDecimal { let scale = self.scale - trailing_count as i64; BigDecimal::new(int_val, scale) } + + ////////////////////////// + // Formatting methods + + /// Create string of this bigdecimal in scientific notation + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n = BigDecimal::from(12345678); + /// assert_eq!(&n.to_scientific_notation(), "1.2345678e7"); + /// ``` + pub fn to_scientific_notation(&self) -> String { + let mut output = String::new(); + self.write_scientific_notation(&mut output).expect("Could not write to string"); + output + } + + /// Write bigdecimal in scientific notation to writer `w` + pub fn write_scientific_notation(&self, w: &mut W) -> fmt::Result { + impl_fmt::write_scientific_notation(self, w) + } + + /// Create string of this bigdecimal in engineering notation + /// + /// Engineering notation is scientific notation with the exponent + /// coerced to a multiple of three + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n = BigDecimal::from(12345678); + /// assert_eq!(&n.to_engineering_notation(), "12.345678e6"); + /// ``` + /// + pub fn to_engineering_notation(&self) -> String { + let mut output = String::new(); + self.write_engineering_notation(&mut output).expect("Could not write to string"); + output + } + + /// Write bigdecimal in engineering notation to writer `w` + pub fn write_engineering_notation(&self, w: &mut W) -> fmt::Result { + impl_fmt::write_engineering_notation(self, w) + } + } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum ParseBigDecimalError { ParseDecimal(ParseFloatError), ParseInt(ParseIntError), @@ -918,15 +992,6 @@ impl From for ParseBigDecimalError { } } -impl FromStr for BigDecimal { - type Err = ParseBigDecimalError; - - #[inline] - fn from_str(s: &str) -> Result { - BigDecimal::from_str_radix(s, 10) - } -} - #[allow(deprecated)] // trim_right_match -> trim_end_match impl Hash for BigDecimal { fn hash(&self, state: &mut H) { @@ -1133,68 +1198,6 @@ impl<'a> Sum<&'a BigDecimal> for BigDecimal { } } -impl fmt::Display for BigDecimal { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Acquire the absolute integer as a decimal string - let mut abs_int = self.int_val.abs().to_str_radix(10); - - // Split the representation at the decimal point - let (before, after) = if self.scale >= abs_int.len() as i64 { - // First case: the integer representation falls - // completely behind the decimal point - let scale = self.scale as usize; - let after = "0".repeat(scale - abs_int.len()) + abs_int.as_str(); - ("0".to_string(), after) - } else { - // Second case: the integer representation falls - // around, or before the decimal point - let location = abs_int.len() as i64 - self.scale; - if location > abs_int.len() as i64 { - // Case 2.1, entirely before the decimal point - // We should prepend zeros - let zeros = location as usize - abs_int.len(); - let abs_int = abs_int + "0".repeat(zeros).as_str(); - (abs_int, "".to_string()) - } else { - // Case 2.2, somewhere around the decimal point - // Just split it in two - let after = abs_int.split_off(location as usize); - (abs_int, after) - } - }; - - // Alter precision after the decimal point - let after = if let Some(precision) = f.precision() { - let len = after.len(); - if len < precision { - after + "0".repeat(precision - len).as_str() - } else { - // TODO: Should we round? - after[0..precision].to_string() - } - } else { - after - }; - - // Concatenate everything - let complete_without_sign = if !after.is_empty() { - before + "." + after.as_str() - } else { - before - }; - - let non_negative = matches!(self.int_val.sign(), Sign::Plus | Sign::NoSign); - //pad_integral does the right thing although we have a decimal - f.pad_integral(non_negative, "", &complete_without_sign) - } -} - -impl fmt::Debug for BigDecimal { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "BigDecimal(\"{}\")", self) - } -} - /// Immutable big-decimal, referencing a borrowed buffer of digits /// @@ -1352,171 +1355,6 @@ impl<'a> From<&'a BigInt> for BigDecimalRef<'a> { } } - -/// Tools to help serializing/deserializing `BigDecimal`s -#[cfg(feature = "serde")] -mod bigdecimal_serde { - use super::*; - use serde::{de, ser}; - - #[allow(unused_imports)] - use num_traits::FromPrimitive; - - impl ser::Serialize for BigDecimal { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - serializer.collect_str(&self) - } - } - - struct BigDecimalVisitor; - - impl<'de> de::Visitor<'de> for BigDecimalVisitor { - type Value = BigDecimal; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a number or formatted decimal string") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - BigDecimal::from_str(value).map_err(|err| E::custom(format!("{}", err))) - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - Ok(BigDecimal::from(value)) - } - - fn visit_i64(self, value: i64) -> Result - where - E: de::Error, - { - Ok(BigDecimal::from(value)) - } - - fn visit_f64(self, value: f64) -> Result - where - E: de::Error, - { - BigDecimal::try_from(value).map_err(|err| E::custom(format!("{}", err))) - } - } - - #[cfg(not(feature = "string-only"))] - impl<'de> de::Deserialize<'de> for BigDecimal { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - d.deserialize_any(BigDecimalVisitor) - } - } - - #[cfg(feature = "string-only")] - impl<'de> de::Deserialize<'de> for BigDecimal { - fn deserialize(d: D) -> Result - where - D: de::Deserializer<'de>, - { - d.deserialize_str(BigDecimalVisitor) - } - } - - #[cfg(test)] - extern crate serde_json; - - #[test] - fn test_serde_serialize() { - let vals = vec![ - ("1.0", "1.0"), - ("0.5", "0.5"), - ("50", "50"), - ("50000", "50000"), - ("1e-3", "0.001"), - ("1e12", "1000000000000"), - ("0.25", "0.25"), - ("12.34", "12.34"), - ("0.15625", "0.15625"), - ("0.3333333333333333", "0.3333333333333333"), - ("3.141592653589793", "3.141592653589793"), - ("94247.77960769380", "94247.77960769380"), - ("10.99", "10.99"), - ("12.0010", "12.0010"), - ]; - for (s, v) in vals { - let expected = format!("\"{}\"", v); - let value = serde_json::to_string(&BigDecimal::from_str(s).unwrap()).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - fn test_serde_deserialize_str() { - let vals = vec![ - ("1.0", "1.0"), - ("0.5", "0.5"), - ("50", "50"), - ("50000", "50000"), - ("1e-3", "0.001"), - ("1e12", "1000000000000"), - ("0.25", "0.25"), - ("12.34", "12.34"), - ("0.15625", "0.15625"), - ("0.3333333333333333", "0.3333333333333333"), - ("3.141592653589793", "3.141592653589793"), - ("94247.77960769380", "94247.77960769380"), - ("10.99", "10.99"), - ("12.0010", "12.0010"), - ]; - for (s, v) in vals { - let expected = BigDecimal::from_str(v).unwrap(); - let value: BigDecimal = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - #[cfg(not(feature = "string-only"))] - fn test_serde_deserialize_int() { - let vals = vec![0, 1, 81516161, -370, -8, -99999999999]; - for n in vals { - let expected = BigDecimal::from_i64(n).unwrap(); - let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); - assert_eq!(expected, value); - } - } - - #[test] - #[cfg(not(feature = "string-only"))] - fn test_serde_deserialize_f64() { - let vals = vec![ - 1.0, - 0.5, - 0.25, - 50.0, - 50000., - 0.001, - 12.34, - 5.0 * 0.03125, - stdlib::f64::consts::PI, - stdlib::f64::consts::PI * 10000.0, - stdlib::f64::consts::PI * 30000.0, - ]; - for n in vals { - let expected = BigDecimal::from_f64(n).unwrap(); - let value: BigDecimal = serde_json::from_str(&serde_json::to_string(&n).unwrap()).unwrap(); - assert_eq!(expected, value); - } - } -} - #[rustfmt::skip] #[cfg(test)] #[allow(non_snake_case)] @@ -2158,75 +1996,6 @@ mod bigdecimal_tests { } } - #[test] - fn test_from_str() { - let vals = vec![ - ("1331.107", 1331107, 3), - ("1.0", 10, 1), - ("2e1", 2, -1), - ("0.00123", 123, 5), - ("-123", -123, 0), - ("-1230", -1230, 0), - ("12.3", 123, 1), - ("123e-1", 123, 1), - ("1.23e+1", 123, 1), - ("1.23E+3", 123, -1), - ("1.23E-8", 123, 10), - ("-1.23E-10", -123, 12), - ("123_", 123, 0), - ("31_862_140.830_686_979", 31862140830686979, 9), - ("-1_1.2_2", -1122, 2), - ("999.521_939", 999521939, 6), - ("679.35_84_03E-2", 679358403, 8), - ("271576662.__E4", 271576662, -4), - ]; - - for &(source, val, scale) in vals.iter() { - let x = BigDecimal::from_str(source).unwrap(); - assert_eq!(x.int_val.to_i64().unwrap(), val); - assert_eq!(x.scale, scale); - } - } - - #[test] - fn test_fmt() { - let vals = vec![ - // b s ( {} {:.1} {:.4} {:4.1} {:+05.1} {:<4.1} - (1, 0, ( "1", "1.0", "1.0000", " 1.0", "+01.0", "1.0 " )), - (1, 1, ( "0.1", "0.1", "0.1000", " 0.1", "+00.1", "0.1 " )), - (1, 2, ( "0.01", "0.0", "0.0100", " 0.0", "+00.0", "0.0 " )), - (1, -2, ("100", "100.0", "100.0000", "100.0", "+100.0", "100.0" )), - (-1, 0, ( "-1", "-1.0", "-1.0000", "-1.0", "-01.0", "-1.0" )), - (-1, 1, ( "-0.1", "-0.1", "-0.1000", "-0.1", "-00.1", "-0.1" )), - (-1, 2, ( "-0.01", "-0.0", "-0.0100", "-0.0", "-00.0", "-0.0" )), - ]; - for (i, scale, results) in vals { - let x = BigDecimal::new(num_bigint::BigInt::from(i), scale); - assert_eq!(format!("{}", x), results.0); - assert_eq!(format!("{:.1}", x), results.1); - assert_eq!(format!("{:.4}", x), results.2); - assert_eq!(format!("{:4.1}", x), results.3); - assert_eq!(format!("{:+05.1}", x), results.4); - assert_eq!(format!("{:<4.1}", x), results.5); - } - } - - #[test] - fn test_debug() { - let vals = vec![ - ("BigDecimal(\"123.456\")", "123.456"), - ("BigDecimal(\"123.400\")", "123.400"), - ("BigDecimal(\"1.20\")", "01.20"), - // ("BigDecimal(\"1.2E3\")", "01.2E3"), <- ambiguous precision - ("BigDecimal(\"1200\")", "01.2E3"), - ]; - - for (expected, source) in vals { - let var = BigDecimal::from_str(source).unwrap(); - assert_eq!(format!("{:?}", var), expected); - } - } - #[test] fn test_signed() { assert!(!BigDecimal::zero().is_positive()); @@ -2247,10 +2016,10 @@ mod bigdecimal_tests { "0.1"), (BigDecimal::new(BigInt::from(132400), -4), BigDecimal::new(BigInt::from(1324), -6), - "1324000000"), + "1.324E+9"), (BigDecimal::new(BigInt::from(1_900_000), 3), BigDecimal::new(BigInt::from(19), -2), - "1900"), + "1.9E+3"), (BigDecimal::new(BigInt::from(0), -3), BigDecimal::zero(), "0"), @@ -2266,47 +2035,6 @@ mod bigdecimal_tests { } } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_nan() { - BigDecimal::from_str("hello").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_empty() { - BigDecimal::from_str("").unwrap(); - } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_invalid_char() { - BigDecimal::from_str("12z3.12").unwrap(); - } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_nan_exponent() { - BigDecimal::from_str("123.123eg").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_empty_exponent() { - BigDecimal::from_str("123.123E").unwrap(); - } - #[test] - #[should_panic(expected = "InvalidDigit")] - fn test_bad_string_multiple_decimal_points() { - BigDecimal::from_str("123.12.45").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_only_decimal() { - BigDecimal::from_str(".").unwrap(); - } - #[test] - #[should_panic(expected = "Empty")] - fn test_bad_string_only_decimal_and_exponent() { - BigDecimal::from_str(".e4").unwrap(); - } - #[test] fn test_from_i128() { let value = BigDecimal::from_i128(-368934881474191032320).unwrap(); @@ -2320,6 +2048,53 @@ mod bigdecimal_tests { let expected = BigDecimal::from_str("668934881474191032320").unwrap(); assert_eq!(value, expected); } + + #[test] + fn test_parse_roundtrip() { + let vals = vec![ + "1.0", + "0.5", + "50", + "50000", + "0.001000000000000000020816681711721685132943093776702880859375", + "0.25", + "12.339999999999999857891452847979962825775146484375", + "0.15625", + "0.333333333333333314829616256247390992939472198486328125", + "3.141592653589793115997963468544185161590576171875", + "31415.926535897931898944079875946044921875", + "94247.779607693795696832239627838134765625", + "1331.107", + "1.0", + "2e1", + "0.00123", + "-123", + "-1230", + "12.3", + "123e-1", + "1.23e+1", + "1.23E+3", + "1.23E-8", + "-1.23E-10", + "123_", + "31_862_140.830_686_979", + "-1_1.2_2", + "999.521_939", + "679.35_84_03E-2", + "271576662.__E4", + // Large decimals with small text representations + "1E10000", + "1E-10000", + "1.129387461293874682630000000487984723987459E10000", + "11293874612938746826340000000087984723987459E10000", + ]; + for s in vals { + let expected = BigDecimal::from_str(s).unwrap(); + let display = format!("{}", expected); + let parsed = BigDecimal::from_str(&display).unwrap(); + assert_eq!(expected, parsed, "[{}] didn't round trip through [{}]", s, display); + } + } } diff --git a/src/rounding.rs b/src/rounding.rs index bbce7f1..0a39655 100644 --- a/src/rounding.rs +++ b/src/rounding.rs @@ -199,273 +199,4 @@ impl RoundingMode { #[cfg(test)] -#[allow(non_snake_case)] -mod test_round_pair { - use paste::paste; - use super::*; - - macro_rules! impl_test { - ( $($mode:ident),+ => $expected:literal) => { - $( - paste! { - #[test] - fn [< mode_ $mode >]() { - let (pair, sign, trailing_zeros) = test_input(); - let mode = self::RoundingMode::$mode; - let result = mode.round_pair(sign, pair, trailing_zeros); - assert_eq!(result, $expected); - } - } - )* - } - } - - macro_rules! define_test_input { - ( - $lhs:literal . $rhs:literal $($t:tt)* ) => { - define_test_input!(sign=Sign::Minus, pair=($lhs, $rhs), $($t)*); - }; - ( $lhs:literal . $rhs:literal $($t:tt)*) => { - define_test_input!(sign=Sign::Plus, pair=($lhs, $rhs), $($t)*); - }; - ( sign=$sign:expr, pair=$pair:expr, ) => { - define_test_input!(sign=$sign, pair=$pair, trailing_zeros=true); - }; - ( sign=$sign:expr, pair=$pair:expr, 000x ) => { - define_test_input!(sign=$sign, pair=$pair, trailing_zeros=false); - }; - ( sign=$sign:expr, pair=$pair:expr, trailing_zeros=$trailing_zeros:literal ) => { - fn test_input() -> ((u8, u8), Sign, bool) { ($pair, $sign, $trailing_zeros) } - }; - } - - mod case_0_1 { - use super::*; - - define_test_input!(0 . 1); - - impl_test!(Up, Ceiling => 1); - impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 0); - } - - mod case_neg_0_1 { - use super::*; - - define_test_input!(-0 . 1); - - impl_test!(Up, Floor => 1); - impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 0); - } - - mod case_0_5 { - use super::*; - - define_test_input!( 0 . 5 ); - - impl_test!(Up, Ceiling, HalfUp => 1); - impl_test!(Down, Floor, HalfDown, HalfEven => 0); - } - - mod case_neg_0_5 { - use super::*; - - define_test_input!(-0 . 5); - - impl_test!(Up, Floor, HalfUp => 1); - impl_test!(Down, Ceiling, HalfDown, HalfEven => 0); - } - - mod case_0_5_000x { - use super::*; - - // ...000x indicates a non-zero trailing digit; affects behavior of rounding N.0 and N.5 - define_test_input!(0 . 5 000x); - - impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Floor => 0); - } - - mod case_neg_0_5_000x { - use super::*; - - define_test_input!(-0 . 5 000x); - - impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Ceiling => 0); - } - - mod case_0_7 { - use super::*; - - define_test_input!(0 . 7); - - impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Floor => 0); - } - - mod case_neg_0_7 { - use super::*; - - define_test_input!(-0 . 7); - - impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); - impl_test!(Down, Ceiling => 0); - } - - mod case_neg_4_3_000x { - use super::*; - - define_test_input!(-4 . 3 000x); - - impl_test!(Up, Floor => 5); - impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 4); - } - - - mod case_9_5_000x { - use super::*; - - define_test_input!(9 . 5 000x); - - impl_test!(Up, Ceiling, HalfDown, HalfUp, HalfEven => 10); - impl_test!(Down, Floor => 9); - } - - mod case_9_5 { - use super::*; - - define_test_input!(9 . 5); - - impl_test!(Up, Ceiling, HalfUp, HalfEven => 10); - impl_test!(Down, Floor, HalfDown => 9); - } - - mod case_8_5 { - use super::*; - - define_test_input!(8 . 5); - - impl_test!(Up, Ceiling, HalfUp => 9); - impl_test!(Down, Floor, HalfDown, HalfEven => 8); - } - - mod case_neg_6_5 { - use super::*; - - define_test_input!(-6 . 5); - - impl_test!(Up, Floor, HalfUp => 7); - impl_test!(Down, Ceiling, HalfDown, HalfEven => 6); - } - - mod case_neg_6_5_000x { - use super::*; - - define_test_input!(-6 . 5 000x); - - impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 7); - impl_test!(Down, Ceiling => 6); - } - - mod case_3_0 { - use super::*; - - define_test_input!(3 . 0); - - impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 3); - } - - mod case_3_0_000x { - use super::*; - - define_test_input!(3 . 0 000x); - - impl_test!(Up, Ceiling => 4); - impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 3); - } - - mod case_neg_2_0 { - use super::*; - - define_test_input!(-2 . 0); - - impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 2); - } - - mod case_neg_2_0_000x { - use super::*; - - define_test_input!(-2 . 0 000x); - - impl_test!(Up, Floor => 3); - impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 2); - } -} - - -#[cfg(test)] -#[allow(non_snake_case)] -mod test_round_u32 { - use paste::paste; - use super::*; - - macro_rules! impl_test { - ( $pos:literal :: $($mode:ident),+ => $expected:literal) => { - $( - paste! { - #[test] - fn [< digit_ $pos _mode_ $mode >]() { - let (value, sign, trailing_zeros) = test_input(); - let mode = self::RoundingMode::$mode; - let pos = stdlib::num::NonZeroU8::new($pos as u8).unwrap(); - let result = mode.round_u32(pos, sign, value, trailing_zeros); - assert_eq!(result, $expected); - } - } - )* - } - } - - macro_rules! define_test_input { - ( - $value:literal $($t:tt)* ) => { - define_test_input!(sign=Sign::Minus, value=$value $($t)*); - }; - ( $value:literal $($t:tt)* ) => { - define_test_input!(sign=Sign::Plus, value=$value $($t)*); - }; - ( sign=$sign:expr, value=$value:literal ...000x ) => { - define_test_input!(sign=$sign, value=$value, trailing_zeros=false); - }; - ( sign=$sign:expr, value=$value:literal ) => { - define_test_input!(sign=$sign, value=$value, trailing_zeros=true); - }; - ( sign=$sign:expr, value=$value:expr, trailing_zeros=$trailing_zeros:literal ) => { - fn test_input() -> (u32, Sign, bool) { ($value, $sign, $trailing_zeros) } - }; - } - - mod case_13950000 { - use super::*; - - define_test_input!(13950000); - - impl_test!(3 :: Up => 13950000); - impl_test!(5 :: Up, Ceiling, HalfUp, HalfEven => 14000000); - impl_test!(5 :: Down, HalfDown => 13900000); - } - - mod case_neg_35488622_000x { - use super::*; - - // ...000x indicates non-zero trailing digit - define_test_input!(-35488622 ...000x); - - impl_test!(1 :: Up => 35488630); - impl_test!(1 :: Down => 35488620); - impl_test!(2 :: Up => 35488700); - impl_test!(2 :: Down => 35488600); - impl_test!(7 :: Up, Floor => 40000000); - impl_test!(7 :: Down, Ceiling => 30000000); - impl_test!(8 :: Up => 100000000); - impl_test!(8 :: Down => 0); - } -} +include!("rounding.tests.rs"); diff --git a/src/rounding.tests.rs b/src/rounding.tests.rs new file mode 100644 index 0000000..f530541 --- /dev/null +++ b/src/rounding.tests.rs @@ -0,0 +1,271 @@ + +#[allow(non_snake_case)] +mod test_round_pair { + use paste::paste; + use super::*; + + macro_rules! impl_test { + ( $($mode:ident),+ => $expected:literal) => { + $( + paste! { + #[test] + fn [< mode_ $mode >]() { + let (pair, sign, trailing_zeros) = test_input(); + let mode = self::RoundingMode::$mode; + let result = mode.round_pair(sign, pair, trailing_zeros); + assert_eq!(result, $expected); + } + } + )* + } + } + + macro_rules! define_test_input { + ( - $lhs:literal . $rhs:literal $($t:tt)* ) => { + define_test_input!(sign=Sign::Minus, pair=($lhs, $rhs), $($t)*); + }; + ( $lhs:literal . $rhs:literal $($t:tt)*) => { + define_test_input!(sign=Sign::Plus, pair=($lhs, $rhs), $($t)*); + }; + ( sign=$sign:expr, pair=$pair:expr, ) => { + define_test_input!(sign=$sign, pair=$pair, trailing_zeros=true); + }; + ( sign=$sign:expr, pair=$pair:expr, 000x ) => { + define_test_input!(sign=$sign, pair=$pair, trailing_zeros=false); + }; + ( sign=$sign:expr, pair=$pair:expr, trailing_zeros=$trailing_zeros:literal ) => { + fn test_input() -> ((u8, u8), Sign, bool) { ($pair, $sign, $trailing_zeros) } + }; + } + + mod case_0_1 { + use super::*; + + define_test_input!(0 . 1); + + impl_test!(Up, Ceiling => 1); + impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 0); + } + + mod case_neg_0_1 { + use super::*; + + define_test_input!(-0 . 1); + + impl_test!(Up, Floor => 1); + impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 0); + } + + mod case_0_5 { + use super::*; + + define_test_input!( 0 . 5 ); + + impl_test!(Up, Ceiling, HalfUp => 1); + impl_test!(Down, Floor, HalfDown, HalfEven => 0); + } + + mod case_neg_0_5 { + use super::*; + + define_test_input!(-0 . 5); + + impl_test!(Up, Floor, HalfUp => 1); + impl_test!(Down, Ceiling, HalfDown, HalfEven => 0); + } + + mod case_0_5_000x { + use super::*; + + // ...000x indicates a non-zero trailing digit; affects behavior of rounding N.0 and N.5 + define_test_input!(0 . 5 000x); + + impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Floor => 0); + } + + mod case_neg_0_5_000x { + use super::*; + + define_test_input!(-0 . 5 000x); + + impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Ceiling => 0); + } + + mod case_0_7 { + use super::*; + + define_test_input!(0 . 7); + + impl_test!(Up, Ceiling, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Floor => 0); + } + + mod case_neg_0_7 { + use super::*; + + define_test_input!(-0 . 7); + + impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 1); + impl_test!(Down, Ceiling => 0); + } + + mod case_neg_4_3_000x { + use super::*; + + define_test_input!(-4 . 3 000x); + + impl_test!(Up, Floor => 5); + impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 4); + } + + + mod case_9_5_000x { + use super::*; + + define_test_input!(9 . 5 000x); + + impl_test!(Up, Ceiling, HalfDown, HalfUp, HalfEven => 10); + impl_test!(Down, Floor => 9); + } + + mod case_9_5 { + use super::*; + + define_test_input!(9 . 5); + + impl_test!(Up, Ceiling, HalfUp, HalfEven => 10); + impl_test!(Down, Floor, HalfDown => 9); + } + + mod case_8_5 { + use super::*; + + define_test_input!(8 . 5); + + impl_test!(Up, Ceiling, HalfUp => 9); + impl_test!(Down, Floor, HalfDown, HalfEven => 8); + } + + mod case_neg_6_5 { + use super::*; + + define_test_input!(-6 . 5); + + impl_test!(Up, Floor, HalfUp => 7); + impl_test!(Down, Ceiling, HalfDown, HalfEven => 6); + } + + mod case_neg_6_5_000x { + use super::*; + + define_test_input!(-6 . 5 000x); + + impl_test!(Up, Floor, HalfUp, HalfDown, HalfEven => 7); + impl_test!(Down, Ceiling => 6); + } + + mod case_3_0 { + use super::*; + + define_test_input!(3 . 0); + + impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 3); + } + + mod case_3_0_000x { + use super::*; + + define_test_input!(3 . 0 000x); + + impl_test!(Up, Ceiling => 4); + impl_test!(Down, Floor, HalfUp, HalfDown, HalfEven => 3); + } + + mod case_neg_2_0 { + use super::*; + + define_test_input!(-2 . 0); + + impl_test!(Up, Down, Ceiling, Floor, HalfUp, HalfDown, HalfEven => 2); + } + + mod case_neg_2_0_000x { + use super::*; + + define_test_input!(-2 . 0 000x); + + impl_test!(Up, Floor => 3); + impl_test!(Down, Ceiling, HalfUp, HalfDown, HalfEven => 2); + } +} + + +#[cfg(test)] +#[allow(non_snake_case)] +mod test_round_u32 { + use paste::paste; + use super::*; + + macro_rules! impl_test { + ( $pos:literal :: $($mode:ident),+ => $expected:literal) => { + $( + paste! { + #[test] + fn [< digit_ $pos _mode_ $mode >]() { + let (value, sign, trailing_zeros) = test_input(); + let mode = self::RoundingMode::$mode; + let pos = stdlib::num::NonZeroU8::new($pos as u8).unwrap(); + let result = mode.round_u32(pos, sign, value, trailing_zeros); + assert_eq!(result, $expected); + } + } + )* + } + } + + macro_rules! define_test_input { + ( - $value:literal $($t:tt)* ) => { + define_test_input!(sign=Sign::Minus, value=$value $($t)*); + }; + ( $value:literal $($t:tt)* ) => { + define_test_input!(sign=Sign::Plus, value=$value $($t)*); + }; + ( sign=$sign:expr, value=$value:literal ...000x ) => { + define_test_input!(sign=$sign, value=$value, trailing_zeros=false); + }; + ( sign=$sign:expr, value=$value:literal ) => { + define_test_input!(sign=$sign, value=$value, trailing_zeros=true); + }; + ( sign=$sign:expr, value=$value:expr, trailing_zeros=$trailing_zeros:literal ) => { + fn test_input() -> (u32, Sign, bool) { ($value, $sign, $trailing_zeros) } + }; + } + + mod case_13950000 { + use super::*; + + define_test_input!(13950000); + + impl_test!(3 :: Up => 13950000); + impl_test!(5 :: Up, Ceiling, HalfUp, HalfEven => 14000000); + impl_test!(5 :: Down, HalfDown => 13900000); + } + + mod case_neg_35488622_000x { + use super::*; + + // ...000x indicates non-zero trailing digit + define_test_input!(-35488622 ...000x); + + impl_test!(1 :: Up => 35488630); + impl_test!(1 :: Down => 35488620); + impl_test!(2 :: Up => 35488700); + impl_test!(2 :: Down => 35488600); + impl_test!(7 :: Up, Floor => 40000000); + impl_test!(7 :: Down, Ceiling => 30000000); + impl_test!(8 :: Up => 100000000); + impl_test!(8 :: Down => 0); + } +}