diff --git a/.circleci/config.yml b/.circleci/config.yml index 5ca96ea..d740dbb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,23 +1,25 @@ version: 2.1 orbs: -# codecov: codecov/codecov@3.0.0 - rust: circleci/rust@1.5.0 +# codecov: codecov/codecov@3.2.4 + rust: circleci/rust@1.6.0 jobs: build-and-test: parameters: rust-version: type: string + default: "1.69.0" debian-version: type: string default: "buster" rust-features: type: string - default: "''" + default: "" docker: - image: rust:<< parameters.rust-version >>-<< parameters.debian-version >> environment: RUSTFLAGS: '-D warnings' + CARGO_NET_GIT_FETCH_WITH_CLI: "true" steps: - checkout - run: @@ -36,10 +38,10 @@ jobs: key: bigdecimal-cargo-<< parameters.rust-version >>-{{ checksum "Cargo.toml" }} - run: name: Build - command: cargo build + command: cargo build << parameters.rust-features >> - run: name: Test - command: cargo test --features=<< parameters.rust-features >> + command: cargo test << parameters.rust-features >> upload-coverage: parameters: @@ -47,7 +49,7 @@ jobs: type: string debian-version: type: string - default: "buster" + default: "bullseye" machine: true steps: - checkout @@ -60,19 +62,20 @@ jobs: -e CI=true $(bash <(curl -s https://codecov.io/env)) akubera/rust-codecov:<< parameters.rust-version >>-<< parameters.debian-version >> - sh -c 'cargo test -q && kcov-rust && upload-kcov-results-to-codecov' + sh -c 'cargo test -q --no-run && kcov-rust && upload-kcov-results-to-codecov' - store_artifacts: path: target/cov - - store_test_results: - path: target/cov + # - store_test_results: + # path: target lint-check: docker: - - image: cimg/rust:1.54.0 + - image: cimg/rust:1.69 steps: - checkout - - rust/build - - rust/format + - rust/build: + with_cache: false + # - rust/format # - rust/clippy - rust/test - run: @@ -81,30 +84,35 @@ jobs: workflows: version: 2 - build-and-test: + cargo:build-and-test: jobs: - - build-and-test: - name: build-and-test-1.34.0 - rust-version: "1.34.0" - debian-version: "stretch" + - lint-check - build-and-test: matrix: parameters: rust-version: - - "1.40.0" - - "1.50.0" + - "1.43.1" - "1.54.0" + + - build-and-test: + name: build-and-test:latest + debian-version: "bullseye" + - build-and-test: matrix: parameters: rust-version: - - "1.50.0" - - "1.54.0" + - "1.43.1" + - "1.69.0" rust-features: - - "'serde'" - - "'serde,string-only'" + - "--features='serde'" + - "--features='serde,string-only'" + + - build-and-test: + name: build-and-test:no-default-features + rust-features: "--no-default-features" + - upload-coverage: - rust-version: "1.54.0" + rust-version: "1.69.0" requires: - - build-and-test-1.54.0 - - lint-check + - build-and-test:latest diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..802b94f --- /dev/null +++ b/.clippy.toml @@ -0,0 +1 @@ +msrv = "1.43.0" diff --git a/.gitignore b/.gitignore index 1878aae..3be4a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ target .vscode/ .*cache/ + +benches/test-data/ + +builds/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 57406fe..9e6e081 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,14 +1,17 @@ cache: - policy: pull-push - # should we only share cache between jobs on one tag? - # key: ${CI_COMMIT_REF_SLUG} - paths: - - .cargo/bin - - .cargo/registry/index - - .cargo/registry/cache - - target/debug/deps - - target/debug/build + - 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 variables: # store cargo registry in the project directory @@ -17,37 +20,125 @@ variables: # 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: "" -.cargo:build-script: &cargo-build-script + # default arguments to pass to cargo test + CARGO_TEST_ARGS: "--verbose" + + +stages: + - check + - build + - test + - deploy + + +.cargo-check-script: &cargo-check-script script: - rustc --version && cargo --version - - cargo build + - 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 - - cargo test --verbose + - printenv CARGO_TEST_ARGS + - cargo test $CARGO_TEST_ARGS + + +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} + policy: pull-push + paths: + - target/debug/deps + - target/debug/build + variables: + RUST_CACHE_KEY: "stable" + <<: *cargo-check-script + +cargo:clippy: + stage: check + image: "akubera/rust:stable" + needs: + - cargo:check + allow_failure: true + variables: + RUST_CACHE_KEY: "stable" + script: + - cargo clippy -- -Dclippy::{dbg_macro,todo} + +cargo:semver-checks: + stage: check + image: "akubera/rust:stable" + needs: + - cargo:check + allow_failure: true + variables: + RUST_CACHE_KEY: "stable" + script: + - cargo semver-checks cargo:build-stable: stage: build - image: "rust:latest" + image: akubera/rust:stable + needs: + - cargo:check + variables: + RUST_CACHE_KEY: "stable" <<: *cargo-build-script - cargo:test-stable: stage: test - image: "rust:latest" + image: akubera/rust:stable needs: - "cargo:build-stable" + variables: + RUST_CACHE_KEY: "stable" + <<: *cargo-test-script + +cargo:build:no-std: + stage: build + image: akubera/rust:stable + needs: + - cargo:check + variables: + RUST_CACHE_KEY: "stable+no_std" + CARGO_BUILD_ARGS: "--no-default-features --lib" <<: *cargo-build-script +cargo:test:no-std: + stage: test + image: akubera/rust:stable + needs: + - "cargo:build:no-std" + variables: + RUST_CACHE_KEY: "stable+no_std" + CARGO_TEST_ARGS: "--no-default-features --lib" + <<: *cargo-test-script + cargo:build-nightly: stage: build image: rustlang/rust:nightly allow_failure: true + variables: + RUST_CACHE_KEY: "nightly" <<: *cargo-build-script @@ -57,69 +148,97 @@ cargo:test-nightly: needs: - cargo:build-nightly allow_failure: true + variables: + RUST_CACHE_KEY: "nightly" <<: *cargo-test-script -cargo:build-1.34: - stage: build - image: "akubera/rust-kcov:1.34.2-stretch" - <<: *cargo-build-script - -cargo:test-1.34: - stage: test - needs: - - cargo:build-1.34 - image: "akubera/rust-kcov:1.34.2-stretch" - allow_failure: true - <<: *cargo-test-script - +cargo:check-1.43: + stage: check + image: "akubera/rust-kcov:1.43.1-buster" + variables: + RUST_CACHE_KEY: "1.43" + <<: *cargo-check-script -cargo:build-1.42: +cargo:build-1.43: stage: build - image: "akubera/rust-kcov:1.42.0-buster" + image: "akubera/rust-kcov:1.43.1-buster" + needs: + - "cargo:check-1.43" + variables: + RUST_CACHE_KEY: "1.43" <<: *cargo-build-script -cargo:test·1.42: +cargo:test-1.43: stage: test needs: - - "cargo:build-1.42" - image: "akubera/rust-kcov:1.42.0-buster" + - "cargo:build-1.43" + image: "akubera/rust-kcov:1.43.1-buster" + variables: + RUST_CACHE_KEY: "1.43" <<: *cargo-test-script +cargo:check-1.54: + stage: check + image: "akubera/rust-kcov:1.54.0-bullseye" + variables: + RUST_CACHE_KEY: "1.54" + <<: *cargo-check-script + cargo:build-1.54: stage: build image: "akubera/rust-kcov:1.54.0-bullseye" + needs: + - "cargo:check-1.54" + variables: + RUST_CACHE_KEY: "1.54" <<: *cargo-build-script -cargo:test·1.54: +cargo:test-1.54: stage: test needs: - "cargo:build-1.54" image: "akubera/rust-kcov:1.54.0-bullseye" + variables: + RUST_CACHE_KEY: "1.54" <<: *cargo-test-script -cargo:build-1.68: +cargo:check-1.70: + stage: check + image: "akubera/rust-grcov:1.70.0-bullseye" + variables: + RUST_CACHE_KEY: "1.70" + <<: *cargo-check-script + +cargo:build-1.70: stage: build - image: "akubera/rust-grcov:1.68.2-bullseye" + image: "akubera/rust-grcov:1.70.0-bullseye" + needs: + - "cargo:check-1.70" + variables: + RUST_CACHE_KEY: "1.70" <<: *cargo-build-script -cargo:test-1.68: +cargo:test-1.70: stage: test needs: - - "cargo:build-1.68" - image: "akubera/rust-grcov:1.68.2-bullseye" + - "cargo:build-1.70" + image: "akubera/rust-grcov:1.70.0-bullseye" + variables: + RUST_CACHE_KEY: "1.70" <<: *cargo-test-script coverage-test: stage: test needs: - - "cargo:test-1.68" - image: "akubera/rust-grcov:1.68.2-bullseye" + - "cargo:test-1.70" + image: "akubera/rust-grcov:1.70.0-bullseye" variables: + RUST_CACHE_KEY: "1.70" CARGO_NET_GIT_FETCH_WITH_CLI: 'true' LLVM_PROFILE_FILE: "target/coverage/%p-%m.profraw" RUSTFLAGS: "-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off " @@ -144,6 +263,26 @@ coverage-test: path: cobertura.xml +cargo:benchmark: + stage: test + needs: + - "cargo:test-1.70" + image: "akubera/bigdecimal-benchmark-base:1.70.0-bullseye" + when: manual + allow_failure: true + cache: [] + variables: + RUST_CACHE_KEY: "1.70" + CARGO_HOME: /usr/local/cargo + BENCHMARK_EXTRAS: "1" + script: + - scripts/benchmark-bigdecimal + artifacts: + paths: + - target/criterion + - "*.html" + + cargo-publish: stage: deploy image: "rust:latest" diff --git a/.rustfmt.toml b/.rustfmt.toml index 33c0ae2..78b231b 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,3 +1,6 @@ - +disable_all_formatting = true +use_small_heuristics = "Off" +reorder_modules = false reorder_imports = false -max_width = 120 +# imports_layout = "HorizontalVertical" +# generics = "Visual" diff --git a/Cargo.toml b/Cargo.toml index b8fc329..8ae8805 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,46 @@ [package] name = "bigdecimal" -version = "0.3.1" +version = "0.4.0" authors = ["Andrew Kubera"] description = "Arbitrary precision decimal numbers" documentation = "https://docs.rs/bigdecimal" homepage = "https://github.com/akubera/bigdecimal-rs" repository = "https://github.com/akubera/bigdecimal-rs" -keywords = ["mathematics", "numerics", "decimal", "arbitrary-precision", "floating-point"] +keywords = [ + "numerics", + "bignum", + "decimal", + "arbitrary-precision", +] +categories = [ "mathematics", "science", "no-std" ] license = "MIT/Apache-2.0" +autobenches = false + +[lib] +bench = false [dependencies] -num-bigint = "0.4" -num-integer = "0.1" -num-traits = "0.2" -serde = { version = "1.0", optional = true } +libm = "0.2.6" +num-bigint = { version = "0.4", default-features = false } +num-integer = { version = "0.1", default-features = false } +num-traits = { version = "0.2", default-features = false } +serde = { version = "1.0", optional = true, default-features = false } -[dev-dependencies.serde_json] -version = "1.0" +[dev-dependencies] +paste = "1" +serde_json = "1.0" +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) +# BENCH: criterion = { version = "0.4", features = [ "html_reports" ] } +# BENCH: oorandom = { version = "11.1.3" } +# BENCH: lazy_static = { version = "1" } [features] +default = ["std"] string-only = [] +std = ["num-bigint/std", "num-integer/std", "num-traits/std"] + +[[bench]] +name = "arithmetic" +harness = false diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 563ae93..fe561ca 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2017 The BigDecimal-rs Contributors +Copyright 2023 The BigDecimal-rs Contributors Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/LICENSE-MIT b/LICENSE-MIT index 5f73012..103bad0 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2017 The BigDecimal-rs Contributors +Copyright (c) 2023 The BigDecimal-rs Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/README.md b/README.md index 830aa32..2644046 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ [![crate](https://img.shields.io/crates/v/bigdecimal.svg)](https://crates.io/crates/bigdecimal) [![Documentation](https://docs.rs/bigdecimal/badge.svg)](https://docs.rs/bigdecimal) -[![minimum rustc 1.34](https://img.shields.io/badge/rustc-1.34+-red.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![minimum rustc 1.43](https://img.shields.io/badge/rustc-1.43+-red.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) + +[![codecov](https://codecov.io/gh/akubera/bigdecimal-rs/branch/feature/circleci/graph/badge.svg?token=YTwyxrxJ3S)](https://codecov.io/gh/akubera/bigdecimal-rs) +[![build status - master](https://gitlab.com/akubera/bigdecimal-rs/badges/master/pipeline.svg?ignore_skipped=true&key_text=status:master&key_width=96)](https://gitlab.com/akubera/bigdecimal-rs/-/pipelines) +[![build status - trunk](https://gitlab.com/akubera/bigdecimal-rs/badges/trunk/pipeline.svg?ignore_skipped=true&key_text=status:trunk&key_width=96)](https://gitlab.com/akubera/bigdecimal-rs/-/pipelines) -[![coverage](https://gitlab.com/akubera/bigdecimal-rs/badges/master/coverage.svg)](https://gitlab.com/akubera/bigdecimal-rs/-/pipelines) -[![build status - master](https://gitlab.com/akubera/bigdecimal-rs/badges/master/pipeline.svg?ignore_skipped=true)](https://gitlab.com/akubera/bigdecimal-rs/-/pipelines) -[![build status - dev](https://gitlab.com/akubera/bigdecimal-rs/badges/devel/pipeline.svg?ignore_skipped=true)](https://gitlab.com/akubera/bigdecimal-rs/-/pipelines) Arbitary-precision decimal numbers implemented in pure Rust. @@ -19,7 +20,7 @@ Add bigdecimal as a dependency to your `Cargo.toml` file: ```toml [dependencies] -bigdecimal = "0.3" +bigdecimal = "0.4" ``` Import and use the `BigDecimal` struct to solve your problems: @@ -40,6 +41,21 @@ sqrt(2) = 1.41421356237309504880168872420969807856967187537694807317667973799073 ``` +#### Default precision + +Default precision may be set at compile time with the environment variable `RUST_BIGDECIMAL_DEFAULT_PRECISION`. +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. + +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. + ## Improvements Work is being done on this codebase again and there are many features diff --git a/benches/arithmetic.rs b/benches/arithmetic.rs new file mode 100644 index 0000000..b6b3299 --- /dev/null +++ b/benches/arithmetic.rs @@ -0,0 +1,344 @@ +//! Benchmarks for arithmetic opertaion + +extern crate criterion; +extern crate bigdecimal; +extern crate oorandom; +extern crate lazy_static; + +use lazy_static::lazy_static; + +use std::fs::File; +use std::time::Duration; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use bigdecimal::BigDecimal; + +#[macro_use] +mod common; +use common::*; + +criterion_main!( + arithmetic, + arithmetic_bulk, +); + +criterion_group!( + name = arithmetic_bulk; + config = Criterion::default() + .measurement_time(Duration::from_secs(7)) + .sample_size(700); + targets = + criterion_benchmark, + datafile_1f633481a742923ab65855c90157bbf7::addition_bulk, +); + +criterion_group!( + name = arithmetic; + config = Criterion::default() + .sample_size(300); + targets = + datafile_9a08ddaa6ce6693cdd7b8a524e088bd0::arithmetic, + datafile_1f633481a742923ab65855c90157bbf7::addition, +); + + +fn make_random_pairs(decs: &[BigDecimal], seed: u64) -> Vec<(&BigDecimal, &BigDecimal)> { + let mut cartesian_pairs = decs + .iter() + .enumerate() + .flat_map(|(i, x)| { + decs.iter().skip(i+1).map(move |y| (x, y)) + }).collect::>(); + + // random number generator from random seed + let mut rng = oorandom::Rand32::new(seed); + + for i in (1..cartesian_pairs.len()).rev() { + let j = rng.rand_u32() as usize % i; + cartesian_pairs.swap(i, j); + } + + cartesian_pairs +} + +fn bench_addition_pairwise(name: &str, c: &mut Criterion, decs: &[BigDecimal]) { + let x_iter = decs.iter().step_by(2); + let y_iter = decs.iter().skip(1).step_by(2); + let pair_iter = std::iter::zip(x_iter, y_iter); + + c.bench_function( + name, + |b| b.iter_batched( + || { + pair_iter.clone() + }, + |pairs| { + for (x, y) in pairs { + black_box(x + y); + } + }, + criterion::BatchSize::SmallInput)); +} + + +fn bench_addition_pairs( + name: &str, + c: &mut Criterion, + dec_pairs: &[(&BigDecimal, &BigDecimal)], +) { + let pair_iter = dec_pairs.iter().cycle(); + + let mut iter_count = 0; + c.bench_function( + name, + |b| b.iter_batched( + || { + iter_count += 1; + pair_iter.clone().skip(iter_count).next().unwrap() + }, + |(x, y)| { + black_box(*x + *y); + }, + criterion::BatchSize::SmallInput)); +} + + +fn bench_addition_pairs_bulk( + name: &str, + c: &mut Criterion, + chunk_size: usize, + dec_pairs: &[(&BigDecimal, &BigDecimal)], +) { + let pair_iter = dec_pairs.iter(); + + let mut iter_count = 0; + c.bench_function( + name, + |b| b.iter_batched( + || { + let skip_by = chunk_size * iter_count; + iter_count += 1; + pair_iter.clone().skip(skip_by).take(chunk_size) + }, + |pairs| { + for (x, y) in pairs { + black_box(*x + *y); + } + }, + criterion::BatchSize::SmallInput)); +} + +fn bench_multiplication_pairs( + name: &str, + c: &mut Criterion, + dec_pairs: &[(&BigDecimal, &BigDecimal)], +) { + let pair_iter = dec_pairs.iter().cycle(); + + let mut iter_count = 0; + c.bench_function( + name, + |b| b.iter_batched( + || { + iter_count += 1; + pair_iter.clone().skip(iter_count).next().unwrap() + }, + |(x, y)| { + black_box(*x * *y); + }, + criterion::BatchSize::SmallInput)); +} + +fn bench_inverse( + name: &str, + c: &mut Criterion, + decs: &[BigDecimal], +) { + let mut idx = 0; + c.bench_function( + name, + |b| b.iter_batched( + || { + idx += 1; + if idx == decs.len() { + idx = 0; + } + &decs[idx] + }, + |x| { + black_box(x.inverse()); + }, + criterion::BatchSize::SmallInput)); +} + + +mod datafile_1f633481a742923ab65855c90157bbf7 { + use super::*; + + + fn get_bigdecimals() -> Vec { + let file = open_benchmark_data_file!("random-bigdecimals-1f633481a742923ab65855c90157bbf7.txt"); + super::read_bigdecimal_file(file) + } + + pub fn addition(c: &mut Criterion) { + let decs = get_bigdecimals(); + let cartesian_pairs = make_random_pairs(&decs, 7238269155957952517_u64); + + bench_addition_pairwise( + "addition-pairwise-1f633481a742923ab65855c90157bbf7", c, &decs + ); + + bench_addition_pairs( + "addition-random-1f633481a742923ab65855c90157bbf7", c, &cartesian_pairs + ); + } + + pub fn addition_bulk(c: &mut Criterion) { + let decs = get_bigdecimals(); + let cartesian_pairs = make_random_pairs(&decs, 7238269155957952517_u64); + bench_addition_pairs_bulk( + "addition-random-1f633481a742923ab65855c90157bbf7-100", c, 100, &cartesian_pairs + ); + } +} + +mod datafile_9a08ddaa6ce6693cdd7b8a524e088bd0 { + use super::*; + + const SRC: &'static str = include_benchmark_data_file!("random-bigdecimals-9a08ddaa6ce6693cdd7b8a524e088bd0.txt"); + + lazy_static! { + static ref BIG_DECIMALS: Vec = super::collect_bigdecimals(SRC); + } + + fn get_bigdecimals<'a>() -> &'a [BigDecimal] { + BIG_DECIMALS.as_slice() + } + + pub fn arithmetic(c: &mut Criterion) { + let decs = get_bigdecimals(); + let cartesian_pairs = make_random_pairs(&decs, 7238269155957952517_u64); + + bench_addition_pairwise( + "addition-pairwise-9a08ddaa6ce6693cdd7b8a524e088bd0", c, &decs + ); + + bench_addition_pairs( + "addition-random-9a08ddaa6ce6693cdd7b8a524e088bd0", c, &cartesian_pairs + ); + + bench_multiplication_pairs( + "multiplication-random-9a08ddaa6ce6693cdd7b8a524e088bd0", c, &cartesian_pairs + ); + + bench_inverse( + "inverse-9a08ddaa6ce6693cdd7b8a524e088bd0", c, &decs + ); + } +} + + +pub fn criterion_benchmark(c: &mut Criterion) { + let src_a = include_benchmark_data_file!("random-bigdecimals-a329e61834832d89593b29f12510bdc8.txt"); + let src_b = include_benchmark_data_file!("random-bigdecimals-4be58192272b15fc67573b39910837d0.txt"); + + let decs_a = collect_bigdecimals(src_a); + let decs_b = collect_bigdecimals(src_b); + assert_ne!(decs_a.len(), 0); + assert_ne!(decs_b.len(), 0); + + let mut decimal_values = Vec::with_capacity(decs_a.len() + decs_b.len()); + for dec in decs_a.iter().chain(decs_b.iter()) { + decimal_values.push(dec) + } + + let mut decimal_pairs = Vec::with_capacity(decs_a.len() * decs_b.len()); + for a in decs_a.iter() { + for b in decs_b.iter() { + decimal_pairs.push((a, b)); + } + } + + let mut random_decimal = RandomIterator::new(&decimal_values); + // let mut random_decimal = decimal_values.iter().cycle(); + let mut random_pairs = RandomIterator::new(&decimal_pairs); + // let mut random_pairs = decimal_pairs.iter().cycle().map(|d| *d); + + c.bench_function( + "addition", + |b| b.iter_batched( + || { + random_pairs.next() + }, + |(a, b)| { + black_box(a + b); + }, + criterion::BatchSize::SmallInput)); + + c.bench_function( + "subtraction", + |b| b.iter_batched( + || { + random_pairs.next() + }, + |(a, b)| { + black_box(a - b); + }, + criterion::BatchSize::SmallInput)); + + c.bench_function( + "multiplication", + |b| b.iter_batched( + || { + random_pairs.next() + }, + |(a, b)| { + black_box(a * b); + }, + criterion::BatchSize::SmallInput)); + + c.bench_function( + "division", + |b| b.iter_batched( + || { + random_pairs.next() + }, + |(a, b)| { + black_box(a / b); + }, + criterion::BatchSize::SmallInput)); + + c.bench_function( + "square", + |b| b.iter_batched( + || { + random_decimal.next() + }, + |a| { + black_box(a.square()); + }, + criterion::BatchSize::SmallInput)); + + c.bench_function( + "cube", + |b| b.iter_batched( + || { + random_decimal.next() + }, + |a| { + black_box(a.cube()); + }, + criterion::BatchSize::SmallInput)); + + c.bench_function( + "sqrt", + |b| b.iter_batched( + || { + random_decimal.next() + }, + |a| { + black_box(a.sqrt()); + }, + criterion::BatchSize::SmallInput)); +} diff --git a/benches/common.rs b/benches/common.rs new file mode 100644 index 0000000..75d523b --- /dev/null +++ b/benches/common.rs @@ -0,0 +1,77 @@ +//! common routines to be included by benches + +use std::fs::File; +use std::io::BufReader; +use bigdecimal::BigDecimal; + +use std::str::FromStr; +use std::io::BufRead; + +macro_rules! resolve_benchmark_data_file { + ( $filename:literal ) => { + concat!(env!("BIGDECIMAL_BENCHMARK_DATA_PATH"), "/", $filename) + } +} + +macro_rules! open_benchmark_data_file { + ( $filename:literal ) => {{ + let path = resolve_benchmark_data_file!($filename); + File::open(path).expect(&format!(concat!("Could not load random datafile ", $filename, " from path {:?}"), path)) + }}; +} + + +macro_rules! include_benchmark_data_file { + ( $filename:literal ) => {{ + include_str!( resolve_benchmark_data_file!($filename) ) + }}; +} + +/// Read vector of big decimals from lines in file +pub fn read_bigdecimal_file(file: File) -> Vec { + read_bigdecimals(BufReader::new(file)) +} + +/// Read bigdecaiml from buffered reader +pub fn read_bigdecimals(reader: R) -> Vec +{ + reader + .lines() + .map(|maybe_string| maybe_string.unwrap()) + .map(|line| BigDecimal::from_str(&line).unwrap()) + .collect() +} + +/// Collect big-decimals from lines in string +pub fn collect_bigdecimals(src: &str) -> Vec { + src.lines() + .map(|line| BigDecimal::from_str(&line).unwrap()) + .collect() +} + + +/// Randomly iterates through items in vector +pub struct RandomIterator<'a, T> { + v: &'a Vec, + rng: oorandom::Rand32, +} + +impl<'a, T: Copy> RandomIterator<'a, T> { + pub fn new(v: &'a Vec) -> Self { + let seed = v.as_ptr() as u64; + Self::new_with_seed(v, seed) + } + + pub fn new_with_seed(v: &'a Vec, seed: u64) -> Self { + Self { + v: v, + rng: oorandom::Rand32::new(seed), + } + } + + pub fn next(&mut self) -> T { + let randval = self.rng.rand_u32() as usize % self.v.len(); + let idx = randval % self.v.len(); + self.v[idx] + } +} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8bc8be9 --- /dev/null +++ b/build.rs @@ -0,0 +1,38 @@ +#![allow(clippy::style)] + + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() -> std::io::Result<()> { + let outdir = match std::env::var_os("OUT_DIR") { + None => return Ok(()), + Some(outdir) => outdir, + }; + let outdir_path = PathBuf::from(outdir); + + write_default_precision(&outdir_path, "default_precision.rs")?; + Ok(()) +} + +/// Create default_precision.rs, containg definition of constant DEFAULT_PRECISION +fn write_default_precision(outdir_path: &PathBuf, filename: &str) -> std::io::Result<()> +{ + + let default_prec = env::var("RUST_BIGDECIMAL_DEFAULT_PRECISION") + .map(|s| s.parse::().expect("$RUST_BIGDECIMAL_DEFAULT_PRECISION must be an integer > 0")) + .map(|nz_num| nz_num.into()) + .unwrap_or(100u32); + + 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)?; + + println!("cargo:rerun-if-changed={}", default_precision_rs_path.display()); + println!("cargo:rerun-if-env-changed={}", "RUST_BIGDECIMAL_DEFAULT_PRECISION"); + + Ok(()) +} diff --git a/scripts/benchmark-bigdecimal b/scripts/benchmark-bigdecimal new file mode 100755 index 0000000..04c08f2 --- /dev/null +++ b/scripts/benchmark-bigdecimal @@ -0,0 +1,52 @@ +#!/bin/sh +# +# Run Criterion Benchmarks +# + +# enable bench-only dependencies in Cargo +sed -i.bak -e 's/# BENCH: //' Cargo.toml + +# Run criterion +cargo bench "$@" + +# Restore Cargo.toml with backup +mv Cargo.toml.bak Cargo.toml + + +# store extra things for the benchmarking report +if [ ! -z "$BENCHMARK_EXTRAS" ]; then +cat < index.html + + + + +BigDecimal Benchmark Results + + + +${CI_COMMIT_SHA} +

BigDecimal Benchmark Results

+

Criterion Report: Report

+EOF + +# Add svg plots to index html +find target/criterion -name 'pdf.svg' -type f -print0 | +sort -z | +while read -d $'\0' svg_file +do + name=$(echo $svg_file | cut -d '/' -f 3) + + sample_datafile=target/criterion/$name/new/sample.json + if [ -f $sample_datafile ]; then + echo "

$name" >> index.html + else + echo "

$name

" >> index.html + fi + echo "" >> index.html +done + +fi diff --git a/scripts/fetch-benchmark-data.sh b/scripts/fetch-benchmark-data.sh new file mode 100755 index 0000000..83af9b1 --- /dev/null +++ b/scripts/fetch-benchmark-data.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# +# Fetch benchmark data +# +# Should this just be a makefile? +# + +TEST_DATA_DIR=benches/test-data + +CURL=$(command -v curl) +WGET=$(command -v wget) + +GITLAB_URL_PATTERN='https://gitlab.com/akubera/bigdecimal-rs/-/raw/data-files/random-test-inputs/' + +function fetch_benchmark_bigdecimal_file() { + local FILENAME="random-bigdecimals-$1.txt" + local FILEPATH=$TEST_DATA_DIR/$FILENAME + + if [ -e $FILEPATH ]; then + echo "exists: $FILEPATH" + else + local URL=${GITLAB_URL_PATTERN///$FILENAME} + echo "fetching: $FILEPATH from $URL" + + if [ $CURL ]; then + $CURL -s --fail -L $URL -o "$FILEPATH" + elif [ $WGET ]; then + $WGET --quiet $URL -O "$FILEPATH" + else + echo "No supported fetching program" + fi + fi +} + +mkdir -p $TEST_DATA_DIR + +fetch_benchmark_bigdecimal_file "1f633481a742923ab65855c90157bbf7" +fetch_benchmark_bigdecimal_file "9a08ddaa6ce6693cdd7b8a524e088bd0" + +fetch_benchmark_bigdecimal_file "4be58192272b15fc67573b39910837d0" +fetch_benchmark_bigdecimal_file "a329e61834832d89593b29f12510bdc8" diff --git a/src/lib.rs b/src/lib.rs index 7c79e95..ff06a97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ // Copyright 2016 Adam Sunderland -// 2016-2017 Andrew Kubera +// 2016-2023 Andrew Kubera // 2017 Ruben De Smet // See the COPYRIGHT file at the top-level directory of this // distribution. @@ -39,12 +39,14 @@ //! //! println!("Input ({}) with 10 decimals: {} vs {})", input, dec, float); //! ``` +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::style)] #![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::match_like_matches_macro)] // requires Rust 1.42.0 + pub extern crate num_bigint; pub extern crate num_traits; @@ -53,16 +55,23 @@ extern crate num_integer; #[cfg(feature = "serde")] extern crate serde; -use std::cmp::Ordering; -use std::convert::TryFrom; -use std::default::Default; -use std::error::Error; -use std::fmt; -use std::hash::{Hash, Hasher}; -use std::num::{ParseFloatError, ParseIntError}; -use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Rem, Sub, SubAssign}; -use std::iter::Sum; -use std::str::{self, FromStr}; +#[cfg(feature = "std")] +include!("./with_std.rs"); + +#[cfg(not(feature = "std"))] +include!("./without_std.rs"); + +// make available some standard items +use self::stdlib::cmp::{self, Ordering}; +use self::stdlib::convert::TryFrom; +use self::stdlib::default::Default; +use self::stdlib::hash::{Hash, Hasher}; +use self::stdlib::num::{ParseFloatError, ParseIntError}; +use self::stdlib::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Rem, Sub, SubAssign}; +use self::stdlib::iter::Sum; +use self::stdlib::str::FromStr; +use self::stdlib::string::{String, ToString}; +use self::stdlib::fmt; use num_bigint::{BigInt, ParseBigIntError, Sign, ToBigInt}; use num_integer::Integer as IntegerTrait; @@ -71,9 +80,20 @@ pub use num_traits::{FromPrimitive, Num, One, Signed, ToPrimitive, Zero}; #[allow(clippy::approx_constant)] // requires Rust 1.43.0 const LOG2_10: f64 = 3.321928094887362_f64; + +// const DEFAULT_PRECISION: u64 = ${RUST_BIGDECIMAL_DEFAULT_PRECISION} or 100; +include!(concat!(env!("OUT_DIR"), "/default_precision.rs")); + #[macro_use] mod macros; +#[cfg(test)] +extern crate paste; + +mod parsing; +pub mod rounding; +pub use rounding::RoundingMode; + #[inline(always)] fn ten_to_the(pow: u64) -> BigInt { if pow < 20 { @@ -154,6 +174,17 @@ pub struct BigDecimal { scale: i64, } +#[cfg(not(feature = "std"))] +// f64::exp2 is only available in std, we have to use an external crate like libm +fn exp2(x: f64) -> f64 { + libm::exp2(x) +} + +#[cfg(feature = "std")] +fn exp2(x: f64) -> f64 { + x.exp2() +} + impl BigDecimal { /// Creates and initializes a `BigDecimal`. /// @@ -180,9 +211,9 @@ impl BigDecimal { /// ``` #[inline] pub fn parse_bytes(buf: &[u8], radix: u32) -> Option { - str::from_utf8(buf) - .ok() - .and_then(|s| BigDecimal::from_str_radix(s, radix).ok()) + stdlib::str::from_utf8(buf) + .ok() + .and_then(|s| BigDecimal::from_str_radix(s, radix).ok()) } /// Return a new BigDecimal object equivalent to self, with internal @@ -211,7 +242,97 @@ impl BigDecimal { } } - #[inline(always)] + /// Return a new BigDecimal after shortening the digits and rounding + /// + /// ``` + /// # use bigdecimal::*; + /// + /// let n: BigDecimal = "129.41675".parse().unwrap(); + /// + /// assert_eq!(n.with_scale_round(2, RoundingMode::Up), "129.42".parse().unwrap()); + /// assert_eq!(n.with_scale_round(-1, RoundingMode::Down), "120".parse().unwrap()); + /// assert_eq!(n.with_scale_round(4, RoundingMode::HalfEven), "129.4168".parse().unwrap()); + /// ``` + pub fn with_scale_round(&self, new_scale: i64, mode: RoundingMode) -> BigDecimal { + use stdlib::cmp::Ordering::*; + + if self.int_val.is_zero() { + return BigDecimal::new(BigInt::zero(), new_scale); + } + + match new_scale.cmp(&self.scale) { + Ordering::Equal => { + self.clone() + } + Ordering::Greater => { + // increase number of zeros + let scale_diff = new_scale - self.scale; + let int_val = &self.int_val * ten_to_the(scale_diff as u64); + BigDecimal::new(int_val, new_scale) + } + Ordering::Less => { + let (sign, mut digits) = self.int_val.to_radix_le(10); + + let digit_count = digits.len(); + let int_digit_count = digit_count as i64 - self.scale; + let rounded_int = match int_digit_count.cmp(&-new_scale) { + Equal => { + let (&last_digit, remaining) = digits.split_last().unwrap(); + let trailing_zeros = remaining.iter().all(Zero::is_zero); + let rounded_digit = mode.round_pair(sign, (0, last_digit), trailing_zeros); + BigInt::new(sign, vec![rounded_digit as u32]) + } + Less => { + debug_assert!(!digits.iter().all(Zero::is_zero)); + let rounded_digit = mode.round_pair(sign, (0, 0), false); + BigInt::new(sign, vec![rounded_digit as u32]) + } + Greater => { + // location of new rounding point + let scale_diff = (self.scale - new_scale) as usize; + + let low_digit = digits[scale_diff - 1]; + let high_digit = digits[scale_diff]; + let trailing_zeros = digits[0..scale_diff-1].iter().all(Zero::is_zero); + let rounded_digit = mode.round_pair(sign, (high_digit, low_digit), trailing_zeros); + + debug_assert!(rounded_digit <= 10); + + if rounded_digit < 10 { + digits[scale_diff] = rounded_digit; + } else { + digits[scale_diff] = 0; + let mut i = scale_diff + 1; + loop { + if i == digit_count { + digits.push(1); + break; + } + + if digits[i] < 9 { + digits[i] += 1; + break; + } + + digits[i] = 0; + i += 1; + } + } + + BigInt::from_radix_le(sign, &digits[scale_diff..], 10).unwrap() + } + }; + + BigDecimal::new(rounded_int, new_scale) + } + } + } + + /// Return a new BigDecimal object with same value and given scale, + /// padding with zeros or truncating digits as needed + /// + /// Useful for aligning decimals before adding/subtracting. + /// fn take_and_scale(mut self, new_scale: i64) -> BigDecimal { if self.int_val.is_zero() { return BigDecimal::new(BigInt::zero(), new_scale); @@ -232,7 +353,19 @@ impl BigDecimal { /// Return a new BigDecimal object with precision set to new value /// - #[inline] + /// ``` + /// # use bigdecimal::*; + /// + /// let n: BigDecimal = "129.41675".parse().unwrap(); + /// + /// assert_eq!(n.with_prec(2), "130".parse().unwrap()); + /// + /// let n_p12 = n.with_prec(12); + /// let (i, scale) = n_p12.as_bigint_and_exponent(); + /// assert_eq!(n_p12, "129.416750000".parse().unwrap()); + /// assert_eq!(i, 129416750000_u64.into()); + /// assert_eq!(scale, 9); + /// ``` pub fn with_prec(&self, prec: u64) -> BigDecimal { let digits = self.digits(); @@ -265,16 +398,17 @@ impl BigDecimal { /// Return the sign of the `BigDecimal` as `num::bigint::Sign`. /// - /// # Examples - /// /// ``` - /// extern crate num_bigint; - /// extern crate bigdecimal; - /// use std::str::FromStr; + /// # use bigdecimal::{BigDecimal, num_bigint::Sign}; + /// + /// fn sign_of(src: &str) -> Sign { + /// let n: BigDecimal = src.parse().unwrap(); + /// n.sign() + /// } /// - /// assert_eq!(bigdecimal::BigDecimal::from_str("-1").unwrap().sign(), num_bigint::Sign::Minus); - /// assert_eq!(bigdecimal::BigDecimal::from_str("0").unwrap().sign(), num_bigint::Sign::NoSign); - /// assert_eq!(bigdecimal::BigDecimal::from_str("1").unwrap().sign(), num_bigint::Sign::Plus); + /// assert_eq!(sign_of("-1"), Sign::Minus); + /// assert_eq!(sign_of("0"), Sign::NoSign); + /// assert_eq!(sign_of("1"), Sign::Plus); /// ``` #[inline] pub fn sign(&self) -> num_bigint::Sign { @@ -287,12 +421,12 @@ impl BigDecimal { /// # Examples /// /// ``` - /// extern crate num_bigint; - /// extern crate bigdecimal; - /// use std::str::FromStr; + /// use bigdecimal::{BigDecimal, num_bigint::BigInt}; /// - /// assert_eq!(bigdecimal::BigDecimal::from_str("1.1").unwrap().as_bigint_and_exponent(), - /// (num_bigint::BigInt::from_str("11").unwrap(), 1)); + /// let n: BigDecimal = "1.23456".parse().unwrap(); + /// let expected = ("123456".parse::().unwrap(), 5); + /// assert_eq!(n.as_bigint_and_exponent(), expected); + /// ``` #[inline] pub fn as_bigint_and_exponent(&self) -> (BigInt, i64) { (self.int_val.clone(), self.scale) @@ -304,12 +438,12 @@ impl BigDecimal { /// # Examples /// /// ``` - /// extern crate num_bigint; - /// extern crate bigdecimal; - /// use std::str::FromStr; + /// use bigdecimal::{BigDecimal, num_bigint::BigInt}; /// - /// assert_eq!(bigdecimal::BigDecimal::from_str("1.1").unwrap().into_bigint_and_exponent(), - /// (num_bigint::BigInt::from_str("11").unwrap(), 1)); + /// let n: BigDecimal = "1.23456".parse().unwrap(); + /// let expected = ("123456".parse::().unwrap(), 5); + /// assert_eq!(n.into_bigint_and_exponent(), expected); + /// ``` #[inline] pub fn into_bigint_and_exponent(self) -> (BigInt, i64) { (self.int_val, self.scale) @@ -323,6 +457,15 @@ impl BigDecimal { } /// Compute the absolute value of number + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n: BigDecimal = "123.45".parse().unwrap(); + /// assert_eq!(n.abs(), "123.45".parse().unwrap()); + /// + /// let n: BigDecimal = "-123.45".parse().unwrap(); + /// assert_eq!(n.abs(), "123.45".parse().unwrap()); + /// ``` #[inline] pub fn abs(&self) -> BigDecimal { BigDecimal { @@ -331,7 +474,13 @@ impl BigDecimal { } } - #[inline] + /// Multiply decimal by 2 (efficiently) + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n: BigDecimal = "123.45".parse().unwrap(); + /// assert_eq!(n.double(), "246.90".parse().unwrap()); + /// ``` pub fn double(&self) -> BigDecimal { if self.is_zero() { self.clone() @@ -343,11 +492,16 @@ impl BigDecimal { } } - /// Divide this efficiently by 2 + /// Divide decimal by 2 (efficiently) /// - /// Note, if this is odd, the precision will increase by 1, regardless - /// of the context's limit. + /// *Note*: If the last digit in the decimal is odd, the precision + /// will increase by 1 /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n: BigDecimal = "123.45".parse().unwrap(); + /// assert_eq!(n.half(), "61.725".parse().unwrap()); + /// ``` #[inline] pub fn half(&self) -> BigDecimal { if self.is_zero() { @@ -365,8 +519,22 @@ impl BigDecimal { } } + /// Square a decimal: *x²* /// - #[inline] + /// No rounding or truncating of digits; this is the full result + /// of the squaring operation. + /// + /// *Note*: doubles the scale of bigdecimal, which might lead to + /// accidental exponential-complexity if used in a loop. + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n: BigDecimal = "1.1156024145937225657484".parse().unwrap(); + /// assert_eq!(n.square(), "1.24456874744734405154288399835406316085210256".parse().unwrap()); + /// + /// let n: BigDecimal = "-9.238597585E+84".parse().unwrap(); + /// assert_eq!(n.square(), "8.5351685337567832225E+169".parse().unwrap()); + /// ``` pub fn square(&self) -> BigDecimal { if self.is_zero() || self.is_one() { self.clone() @@ -378,7 +546,22 @@ impl BigDecimal { } } - #[inline] + /// Cube a decimal: *x³* + /// + /// No rounding or truncating of digits; this is the full result + /// of the cubing operation. + /// + /// *Note*: triples the scale of bigdecimal, which might lead to + /// accidental exponential-complexity if used in a loop. + /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n: BigDecimal = "1.1156024145937225657484".parse().unwrap(); + /// assert_eq!(n.cube(), "1.388443899780141911774491376394890472130000455312878627147979955904".parse().unwrap()); + /// + /// let n: BigDecimal = "-9.238597585E+84".parse().unwrap(); + /// assert_eq!(n.cube(), "-7.88529874035334084567570176625E+254".parse().unwrap()); + /// ``` pub fn cube(&self) -> BigDecimal { if self.is_zero() || self.is_one() { self.clone() @@ -392,8 +575,19 @@ impl BigDecimal { /// Take the square root of the number /// + /// Uses default-precision, set from build time environment variable + //// `RUST_BIGDECIMAL_DEFAULT_PRECISION` (defaults to 100) + /// /// If the value is < 0, None is returned /// + /// ``` + /// # use bigdecimal::BigDecimal; + /// let n: BigDecimal = "1.1156024145937225657484".parse().unwrap(); + /// assert_eq!(n.sqrt().unwrap(), "1.056220817156016181190291268045893004363809142172289919023269377496528394924695970851558013658193913".parse().unwrap()); + /// + /// let n: BigDecimal = "-9.238597585E+84".parse().unwrap(); + /// assert_eq!(n.sqrt(), None); + /// ``` #[inline] pub fn sqrt(&self) -> Option { if self.is_zero() || self.is_one() { @@ -407,7 +601,8 @@ impl BigDecimal { let guess = { let magic_guess_scale = 1.1951678538495576_f64; let initial_guess = (self.int_val.bits() as f64 - self.scale as f64 * LOG2_10) / 2.0; - let res = magic_guess_scale * initial_guess.exp2(); + let res = magic_guess_scale * exp2(initial_guess); + if res.is_normal() { BigDecimal::try_from(res).unwrap() } else { @@ -423,7 +618,7 @@ impl BigDecimal { // } // TODO: Use context variable to set precision - let max_precision = 100; + let max_precision = DEFAULT_PRECISION; let next_iteration = move |r: BigDecimal| { // division needs to be precise to (at least) one extra digit @@ -479,7 +674,8 @@ impl BigDecimal { let guess = { let magic_guess_scale = 1.124960491619939_f64; let initial_guess = (self.int_val.bits() as f64 - self.scale as f64 * LOG2_10) / 3.0; - let res = magic_guess_scale * initial_guess.exp2(); + let res = magic_guess_scale * exp2(initial_guess); + if res.is_normal() { BigDecimal::try_from(res).unwrap() } else { @@ -490,7 +686,7 @@ impl BigDecimal { }; // TODO: Use context variable to set precision - let max_precision = 100; + let max_precision = DEFAULT_PRECISION; let three = BigDecimal::from(3); @@ -546,7 +742,7 @@ impl BigDecimal { let magic_factor = 0.721507597259061_f64; let initial_guess = scale * LOG2_10 - bits; - let res = magic_factor * initial_guess.exp2(); + let res = magic_factor * exp2(initial_guess); if res.is_normal() { BigDecimal::try_from(res).unwrap() @@ -557,7 +753,7 @@ impl BigDecimal { } }; - let max_precision = 100; + let max_precision = DEFAULT_PRECISION; let next_iteration = move |r: BigDecimal| { let two = BigDecimal::from(2); let tmp = two - self * &r; @@ -593,28 +789,94 @@ impl BigDecimal { /// Return number rounded to round_digits precision after the decimal point pub fn round(&self, round_digits: i64) -> BigDecimal { - let (bigint, decimal_part_digits) = self.as_bigint_and_exponent(); - let need_to_round_digits = decimal_part_digits - round_digits; - if round_digits >= 0 && need_to_round_digits <= 0 { - return self.clone(); + // we have fewer digits than we need, no rounding + if round_digits >= self.scale { + return self.with_scale(round_digits); } - let mut number = bigint.clone(); - if number < BigInt::zero() { - number = -number; + let (sign, double_digits) = self.int_val.to_radix_le(100); + + let last_is_double_digit = *double_digits.last().unwrap() >= 10; + let digit_count = (double_digits.len() - 1) * 2 + 1 + last_is_double_digit as usize; + + // relevant digit positions: each "pos" is position of 10^{pos} + let least_significant_pos = -self.scale; + let most_sig_digit_pos = digit_count as i64 + least_significant_pos - 1; + let rounding_pos = -round_digits; + + // digits are too small, round to zero + if rounding_pos > most_sig_digit_pos + 1 { + return BigDecimal::zero(); } - for _ in 0..(need_to_round_digits - 1) { - number /= 10; + + // highest digit is next to rounding point + if rounding_pos == most_sig_digit_pos + 1 { + let (&last_double_digit, remaining) = double_digits.split_last().unwrap(); + + let mut trailing_zeros = remaining.iter().all(|&d| d == 0); + + let last_digit = if last_is_double_digit { + let (high, low) = num_integer::div_rem(last_double_digit, 10); + trailing_zeros &= low == 0; + high + } else { + last_double_digit + }; + + if last_digit > 5 || (last_digit == 5 && !trailing_zeros) { + return BigDecimal::new(BigInt::one(), round_digits); + } + + return BigDecimal::zero(); } - let digit = number % 10; - if digit <= BigInt::from(4) { - self.with_scale(round_digits) - } else if bigint.is_negative() { - self.with_scale(round_digits) - BigDecimal::new(BigInt::from(1), round_digits) + let double_digits_to_remove = self.scale - round_digits; + debug_assert!(double_digits_to_remove > 0); + + let (rounding_idx, rounding_offset) = num_integer::div_rem(double_digits_to_remove as usize, 2); + debug_assert!(rounding_idx <= double_digits.len()); + + let (low_digits, high_digits) = double_digits.as_slice().split_at(rounding_idx); + debug_assert!(high_digits.len() > 0); + + let mut unrounded_uint = num_bigint::BigUint::from_radix_le(high_digits, 100).unwrap(); + + let rounded_uint; + if rounding_offset == 0 { + let high_digit = high_digits[0] % 10; + let (&top, rest) = low_digits.split_last().unwrap_or((&0u8, &[])); + let (low_digit, lower_digit) = num_integer::div_rem(top, 10); + let trailing_zeros = lower_digit == 0 && rest.iter().all(|&d| d == 0); + + let rounding = if low_digit < 5 { + 0 + } else if low_digit > 5 || !trailing_zeros { + 1 + } else { + high_digit % 2 + }; + + rounded_uint = unrounded_uint + rounding; } else { - self.with_scale(round_digits) + BigDecimal::new(BigInt::from(1), round_digits) + let (high_digit, low_digit) = num_integer::div_rem(high_digits[0], 10); + + let trailing_zeros = low_digits.iter().all(|&d| d == 0); + + let rounding = if low_digit < 5 { + 0 + } else if low_digit > 5 || !trailing_zeros { + 1 + } else { + high_digit % 2 + }; + + // shift unrounded_uint down, + unrounded_uint /= num_bigint::BigUint::from_u8(10).unwrap(); + rounded_uint = unrounded_uint + rounding; } + + let rounded_int = num_bigint::BigInt::from_biguint(sign, rounded_uint); + BigDecimal::new(rounded_int, round_digits) } /// Return true if this number has zero fractional part (is equal @@ -637,6 +899,8 @@ impl BigDecimal { return BigDecimal::one(); } + let target_precision = DEFAULT_PRECISION; + let precision = self.digits(); let mut term = self.clone(); @@ -650,13 +914,13 @@ impl BigDecimal { // ∑ term=x^n/n! result += impl_division(term.int_val.clone(), &factorial, term.scale, 117 + precision); - let trimmed_result = result.with_prec(105); + let trimmed_result = result.with_prec(target_precision + 5); if prev_result == trimmed_result { - return trimmed_result.with_prec(100); + return trimmed_result.with_prec(target_precision); } prev_result = trimmed_result; } - return result.with_prec(100); + unreachable!("Loop did not converge") } #[must_use] @@ -666,7 +930,7 @@ impl BigDecimal { } let (sign, mut digits) = self.int_val.to_radix_be(10); let trailing_count = digits.iter().rev().take_while(|i| **i == 0).count(); - let trunc_to = digits.len() - trailing_count as usize; + let trunc_to = digits.len() - trailing_count; digits.truncate(trunc_to); let int_val = BigInt::from_radix_be(sign, &digits, 10).unwrap(); let scale = self.scale - trailing_count as i64; @@ -697,7 +961,8 @@ impl fmt::Display for ParseBigDecimalError { } } -impl Error for ParseBigDecimalError { +#[cfg(feature = "std")] +impl std::error::Error for ParseBigDecimalError { fn description(&self) -> &str { "failed to parse bigint/biguint" } @@ -984,7 +1249,7 @@ impl<'a> AddAssign<&'a BigDecimal> for BigDecimal { } } -impl<'a> AddAssign for BigDecimal { +impl AddAssign for BigDecimal { #[inline] fn add_assign(&mut self, rhs: BigInt) { *self += BigDecimal::new(rhs, 0) @@ -1013,7 +1278,7 @@ impl Sub for BigDecimal { #[inline] fn sub(self, rhs: BigDecimal) -> BigDecimal { let mut lhs = self; - let scale = std::cmp::max(lhs.scale, rhs.scale); + let scale = cmp::max(lhs.scale, rhs.scale); match lhs.scale.cmp(&rhs.scale) { Ordering::Equal => { @@ -1032,7 +1297,7 @@ impl<'a> Sub<&'a BigDecimal> for BigDecimal { #[inline] fn sub(self, rhs: &BigDecimal) -> BigDecimal { let mut lhs = self; - let scale = std::cmp::max(lhs.scale, rhs.scale); + let scale = cmp::max(lhs.scale, rhs.scale); match lhs.scale.cmp(&rhs.scale) { Ordering::Equal => { @@ -1151,7 +1416,7 @@ impl<'a> SubAssign<&'a BigDecimal> for BigDecimal { } } -impl<'a> SubAssign for BigDecimal { +impl SubAssign for BigDecimal { #[inline(always)] fn sub_assign(&mut self, rhs: BigInt) { *self -= BigDecimal::new(rhs, 0) @@ -1288,7 +1553,7 @@ impl Mul for BigInt { fn mul(mut self, mut rhs: BigDecimal) -> BigDecimal { if rhs.is_one() { rhs.scale = 0; - std::mem::swap(&mut rhs.int_val, &mut self); + stdlib::mem::swap(&mut rhs.int_val, &mut self); } else if !self.is_one() { rhs.int_val *= self; } @@ -1296,7 +1561,6 @@ impl Mul for BigInt { } } - impl<'a> Mul for &'a BigInt { type Output = BigDecimal; @@ -1316,7 +1580,6 @@ impl<'a> Mul for &'a BigInt { } } - impl<'a, 'b> Mul<&'a BigDecimal> for &'b BigInt { type Output = BigDecimal; @@ -1349,7 +1612,6 @@ impl<'a> Mul<&'a BigDecimal> for BigInt { } } - forward_val_assignop!(impl MulAssign for BigDecimal, mul_assign); impl<'a> MulAssign<&'a BigDecimal> for BigDecimal { @@ -1380,8 +1642,6 @@ impl MulAssign for BigDecimal { } } - - impl_div_for_primitives!(); #[inline(always)] @@ -1460,7 +1720,7 @@ impl Div for BigDecimal { }; } - let max_precision = 100; + let max_precision = DEFAULT_PRECISION; return impl_division(self.int_val, &other.int_val, scale, max_precision); } @@ -1486,7 +1746,7 @@ impl<'a> Div<&'a BigDecimal> for BigDecimal { }; } - let max_precision = 100; + let max_precision = DEFAULT_PRECISION; return impl_division(self.int_val, &other.int_val, scale, max_precision); } @@ -1519,7 +1779,7 @@ impl<'a, 'b> Div<&'b BigDecimal> for &'a BigDecimal { }; } - let max_precision = 100; + let max_precision = DEFAULT_PRECISION; return impl_division(num_int.clone(), den_int, scale, max_precision); } @@ -1530,7 +1790,7 @@ impl Rem for BigDecimal { #[inline] fn rem(self, other: BigDecimal) -> BigDecimal { - let scale = std::cmp::max(self.scale, other.scale); + let scale = cmp::max(self.scale, other.scale); let num = self.take_and_scale(scale).int_val; let den = other.take_and_scale(scale).int_val; @@ -1544,7 +1804,7 @@ impl<'a> Rem<&'a BigDecimal> for BigDecimal { #[inline] fn rem(self, other: &BigDecimal) -> BigDecimal { - let scale = std::cmp::max(self.scale, other.scale); + let scale = cmp::max(self.scale, other.scale); let num = self.take_and_scale(scale).int_val; let den = &other.int_val; @@ -1561,7 +1821,7 @@ impl<'a> Rem for &'a BigDecimal { #[inline] fn rem(self, other: BigDecimal) -> BigDecimal { - let scale = std::cmp::max(self.scale, other.scale); + let scale = cmp::max(self.scale, other.scale); let num = &self.int_val; let den = other.take_and_scale(scale).int_val; @@ -1581,7 +1841,7 @@ impl<'a, 'b> Rem<&'b BigDecimal> for &'a BigDecimal { #[inline] fn rem(self, other: &BigDecimal) -> BigDecimal { - let scale = std::cmp::max(self.scale, other.scale); + let scale = cmp::max(self.scale, other.scale); let num = &self.int_val; let den = &other.int_val; @@ -1673,7 +1933,7 @@ impl<'a> Sum<&'a BigDecimal> for BigDecimal { impl fmt::Display for BigDecimal { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Aquire the absolute integer as a decimal string + // 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 @@ -1691,7 +1951,7 @@ impl fmt::Display for BigDecimal { // 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 usize).as_str(); + let abs_int = abs_int + "0".repeat(zeros).as_str(); (abs_int, "".to_string()) } else { // Case 2.2, somewhere around the decimal point @@ -1721,10 +1981,7 @@ impl fmt::Display for BigDecimal { before }; - let non_negative = match self.int_val.sign() { - Sign::Plus | Sign::NoSign => true, - _ => false, - }; + 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) } @@ -1792,7 +2049,10 @@ impl Num for BigDecimal { // copy all trailing characters after '.' into the digits string digits.push_str(trail); - (digits, trail.len() as i64) + // count number of trailing digits + let trail_digits = trail.chars().filter(|c| *c != '_').count(); + + (digits, trail_digits as i64) } }; @@ -1810,6 +2070,12 @@ impl ToPrimitive for BigDecimal { 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(), @@ -1817,6 +2083,13 @@ impl ToPrimitive for BigDecimal { 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)) @@ -1843,6 +2116,24 @@ impl From for BigDecimal { } } +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] fn from((int_val, scale): (BigInt, i64)) -> Self { @@ -1888,7 +2179,7 @@ impl TryFrom for BigDecimal { #[inline] fn try_from(n: f32) -> Result { - BigDecimal::from_str(&format!("{:.PRECISION$e}", n, PRECISION = ::std::f32::DIGITS as usize)) + parsing::try_parse_from_f32(n) } } @@ -1897,7 +2188,7 @@ impl TryFrom for BigDecimal { #[inline] fn try_from(n: f64) -> Result { - BigDecimal::from_str(&format!("{:.PRECISION$e}", n, PRECISION = ::std::f64::DIGITS as usize)) + parsing::try_parse_from_f64(n) } } @@ -1912,6 +2203,16 @@ impl FromPrimitive for BigDecimal { 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() @@ -1932,11 +2233,9 @@ impl ToBigInt for BigDecimal { /// Tools to help serializing/deserializing `BigDecimal`s #[cfg(feature = "serde")] mod bigdecimal_serde { - use super::BigDecimal; + use super::*; use serde::{de, ser}; - use std::convert::TryFrom; - use std::fmt; - use std::str::FromStr; + #[allow(unused_imports)] use num_traits::FromPrimitive; @@ -2012,8 +2311,6 @@ mod bigdecimal_serde { #[test] fn test_serde_serialize() { - use std::str::FromStr; - let vals = vec![ ("1.0", "1.0"), ("0.5", "0.5"), @@ -2039,8 +2336,6 @@ mod bigdecimal_serde { #[test] fn test_serde_deserialize_str() { - use std::str::FromStr; - let vals = vec![ ("1.0", "1.0"), ("0.5", "0.5"), @@ -2087,9 +2382,9 @@ mod bigdecimal_serde { 0.001, 12.34, 5.0 * 0.03125, - ::std::f64::consts::PI, - ::std::f64::consts::PI * 10000.0, - ::std::f64::consts::PI * 30000.0, + 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(); @@ -2101,12 +2396,12 @@ mod bigdecimal_serde { #[rustfmt::skip] #[cfg(test)] +#[allow(non_snake_case)] mod bigdecimal_tests { - use BigDecimal; + use crate::{stdlib, BigDecimal, ToString, FromStr, TryFrom}; use num_traits::{ToPrimitive, FromPrimitive, Signed, Zero, One}; - use std::convert::TryFrom; - use std::str::FromStr; use num_bigint; + use paste::paste; #[test] fn test_sum() { @@ -2116,7 +2411,7 @@ mod bigdecimal_tests { BigDecimal::from_f32(0.001).unwrap(), ]; - let expected_sum = BigDecimal::from_f32(2.801).unwrap(); + let expected_sum = BigDecimal::from_str("2.801000011968426406383514404296875").unwrap(); let sum = vals.iter().sum::(); assert_eq!(expected_sum, sum); @@ -2127,10 +2422,9 @@ mod bigdecimal_tests { let vals = vec![ BigDecimal::from_f32(0.1).unwrap(), BigDecimal::from_f32(0.2).unwrap(), - // BigDecimal::from_f32(0.001).unwrap(), ]; - let expected_sum = BigDecimal::from_f32(0.3).unwrap(); + let expected_sum = BigDecimal::from_str("0.300000004470348358154296875").unwrap(); let sum = vals.iter().sum::(); assert_eq!(expected_sum, sum); @@ -2154,6 +2448,39 @@ mod bigdecimal_tests { } } + #[test] + fn test_to_i128() { + let vals = vec![ + ("170141183460469231731687303715884105727", 170141183460469231731687303715884105727), + ("-170141183460469231731687303715884105728", -170141183460469231731687303715884105728), + ("12.34", 12), + ("3.14", 3), + ("50", 50), + ("0.001", 0), + ]; + for (s, ans) in vals { + let calculated = BigDecimal::from_str(s).unwrap().to_i128().unwrap(); + + assert_eq!(ans, calculated); + } + } + + #[test] + fn test_to_u128() { + let vals = vec![ + ("340282366920938463463374607431768211455", 340282366920938463463374607431768211455), + ("12.34", 12), + ("3.14", 3), + ("50", 50), + ("0.001", 0), + ]; + for (s, ans) in vals { + let calculated = BigDecimal::from_str(s).unwrap().to_u128().unwrap(); + + assert_eq!(ans, calculated); + } + } + #[test] fn test_to_f64() { let vals = vec![ @@ -2179,8 +2506,8 @@ mod bigdecimal_tests { ("12", 12), ("-13", -13), ("111", 111), - ("-128", ::std::i8::MIN), - ("127", ::std::i8::MAX), + ("-128", i8::MIN), + ("127", i8::MAX), ]; for (s, n) in vals { let expected = BigDecimal::from_str(s).unwrap(); @@ -2192,28 +2519,27 @@ mod bigdecimal_tests { #[test] fn test_from_f32() { let vals = vec![ + ("0.0", 0.0), ("1.0", 1.0), ("0.5", 0.5), ("0.25", 0.25), ("50.", 50.0), ("50000", 50000.), - ("0.001", 0.001), - ("12.34", 12.34), - ("0.15625", 5.0 * 0.03125), - ("3.141593", ::std::f32::consts::PI), - ("31415.93", ::std::f32::consts::PI * 10000.0), - ("94247.78", ::std::f32::consts::PI * 30000.0), - // ("3.14159265358979323846264338327950288f32", ::std::f32::consts::PI), - + ("0.001000000047497451305389404296875", 0.001), + ("12.340000152587890625", 12.34), + ("0.15625", 0.15625), + ("3.1415927410125732421875", stdlib::f32::consts::PI), + ("31415.927734375", stdlib::f32::consts::PI * 10000.0), + ("94247.78125", stdlib::f32::consts::PI * 30000.0), + ("1048576", 1048576.), ]; for (s, n) in vals { let expected = BigDecimal::from_str(s).unwrap(); let value = BigDecimal::from_f32(n).unwrap(); assert_eq!(expected, value); - // assert_eq!(expected, n); } - } + #[test] fn test_from_f64() { let vals = vec![ @@ -2221,15 +2547,14 @@ mod bigdecimal_tests { ("0.5", 0.5), ("50", 50.), ("50000", 50000.), - ("1e-3", 0.001), + ("0.001000000000000000020816681711721685132943093776702880859375", 0.001), ("0.25", 0.25), - ("12.34", 12.34), - // ("12.3399999999999999", 12.34), // <- Precision 16 decimal points + ("12.339999999999999857891452847979962825775146484375", 12.34), ("0.15625", 5.0 * 0.03125), - ("0.3333333333333333", 1.0 / 3.0), - ("3.141592653589793", ::std::f64::consts::PI), - ("31415.92653589793", ::std::f64::consts::PI * 10000.0f64), - ("94247.77960769380", ::std::f64::consts::PI * 30000.0f64), + ("0.333333333333333314829616256247390992939472198486328125", 1.0 / 3.0), + ("3.141592653589793115997963468544185161590576171875", stdlib::f64::consts::PI), + ("31415.926535897931898944079875946044921875", stdlib::f64::consts::PI * 10000.0f64), + ("94247.779607693795696832239627838134765625", stdlib::f64::consts::PI * 30000.0f64), ]; for (s, n) in vals { let expected = BigDecimal::from_str(s).unwrap(); @@ -2241,8 +2566,8 @@ mod bigdecimal_tests { #[test] fn test_nan_float() { - assert!(BigDecimal::try_from(std::f32::NAN).is_err()); - assert!(BigDecimal::try_from(std::f64::NAN).is_err()); + assert!(BigDecimal::try_from(f32::NAN).is_err()); + assert!(BigDecimal::try_from(f64::NAN).is_err()); } #[test] @@ -2508,8 +2833,8 @@ mod bigdecimal_tests { #[test] fn test_hash_equal() { - use std::hash::{Hash, Hasher}; - use std::collections::hash_map::DefaultHasher; + use stdlib::DefaultHasher; + use stdlib::hash::{Hash, Hasher}; fn hash(obj: &T) -> u64 where T: Hash @@ -2545,8 +2870,8 @@ mod bigdecimal_tests { #[test] fn test_hash_not_equal() { - use std::hash::{Hash, Hasher}; - use std::collections::hash_map::DefaultHasher; + use stdlib::DefaultHasher; + use stdlib::hash::{Hash, Hasher}; fn hash(obj: &T) -> u64 where T: Hash @@ -2572,8 +2897,8 @@ mod bigdecimal_tests { #[test] fn test_hash_equal_scale() { - use std::hash::{Hash, Hasher}; - use std::collections::hash_map::DefaultHasher; + use stdlib::DefaultHasher; + use stdlib::hash::{Hash, Hasher}; fn hash(obj: &T) -> u64 where T: Hash @@ -2736,19 +3061,32 @@ mod bigdecimal_tests { #[test] fn test_round() { let test_cases = vec![ - ("1.45", 1, "1.5"), + ("1.45", 1, "1.4"), ("1.444445", 1, "1.4"), ("1.44", 1, "1.4"), ("0.444", 2, "0.44"), + ("4.5", 0, "4"), + ("4.05", 1, "4.0"), + ("4.050", 1, "4.0"), + ("4.15", 1, "4.2"), ("0.0045", 2, "0.00"), + ("5.5", -1, "10"), ("-1.555", 2, "-1.56"), ("-1.555", 99, "-1.555"), ("5.5", 0, "6"), ("-1", -1, "0"), - ("5", -1, "10"), + ("5", -1, "0"), ("44", -1, "40"), ("44", -99, "0"), + ("44", 99, "44"), + ("1.4499999999", -1, "0"), + ("1.4499999999", 0, "1"), ("1.4499999999", 1, "1.4"), + ("1.4499999999", 2, "1.45"), + ("1.4499999999", 3, "1.450"), + ("1.4499999999", 4, "1.4500"), + ("1.4499999999", 10, "1.4499999999"), + ("1.4499999999", 15, "1.449999999900000"), ("-1.4499999999", 1, "-1.4"), ("1.449999999", 1, "1.4"), ("-9999.444455556666", 10, "-9999.4444555567"), @@ -2767,6 +3105,17 @@ mod bigdecimal_tests { } } + #[test] + fn round_large_number() { + use super::BigDecimal; + + let z = BigDecimal::from_str("3.4613133327063255443352353815722045816611958409944513040035462804475524").unwrap(); + let expected = BigDecimal::from_str("11.9806899871705702711783103817684242408972124568942276285200973527647213").unwrap(); + let zsq = &z*&z; + let zsq = zsq.round(70); + debug_assert_eq!(zsq, expected); + } + #[test] fn test_is_integer() { let true_vals = vec![ @@ -2887,23 +3236,10 @@ mod bigdecimal_tests { } } - #[test] - fn test_double() { - let vals = vec![ - ("1", "2"), - ("1.00", "2.00"), - ("1.50", "3.00"), - ("5", "10"), - ("5.0", "10.0"), - ("5.5", "11.0"), - ("5.05", "10.10"), - ]; - for &(x, y) in vals.iter() { - let a = BigDecimal::from_str(x).unwrap().double(); - let b = BigDecimal::from_str(y).unwrap(); - assert_eq!(a, b); - assert_eq!(a.scale, b.scale); - } + mod double { + use super::*; + + include!("lib.tests.double.rs"); } #[test] @@ -2989,11 +3325,17 @@ mod bigdecimal_tests { ("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_i32().unwrap(), val); + assert_eq!(x.int_val.to_i64().unwrap(), val); assert_eq!(x.scale, scale); } } @@ -3116,4 +3458,28 @@ mod bigdecimal_tests { 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(); + let expected = BigDecimal::from_str("-368934881474191032320").unwrap(); + assert_eq!(value, expected); + } + + #[test] + fn test_from_u128() { + let value = BigDecimal::from_u128(668934881474191032320).unwrap(); + let expected = BigDecimal::from_str("668934881474191032320").unwrap(); + assert_eq!(value, expected); + } +} + + +#[cfg(test)] +#[allow(non_snake_case)] +mod test_with_scale_round { + use super::*; + use paste::paste; + + include!("lib.tests.with_scale_round.rs"); } diff --git a/src/lib.tests.double.rs b/src/lib.tests.double.rs new file mode 100644 index 0000000..0f71390 --- /dev/null +++ b/src/lib.tests.double.rs @@ -0,0 +1,25 @@ +// Test BigDecimal::double + +macro_rules! impl_case { + ($name:ident : $a:literal => $ex:literal ) => { + paste! { + #[test] + fn $name() { + let value = BigDecimal::from_str($a).unwrap(); + let expected = BigDecimal::from_str($ex).unwrap(); + let result = value.double(); + assert_eq!(result, expected); + assert_eq!(result.int_val, expected.int_val); + assert_eq!(result.scale, expected.scale); + } + } + }; +} + +impl_case!(case_zero : "0" => "0"); +impl_case!(case_1 : "1" => "2"); +impl_case!(case_100Em2 : "1.00" => "2.00"); +impl_case!(case_150Em2 : "1.50" => "3.00"); +impl_case!(case_neg150Em2 : "-1.50" => "-3.00"); +impl_case!(case_32909E4 : "32909E4" => "6.5818E+8"); +impl_case!(case_1_1156024145937225657484 : "1.1156024145937225657484" => "2.2312048291874451314968"); diff --git a/src/lib.tests.with_scale_round.rs b/src/lib.tests.with_scale_round.rs new file mode 100644 index 0000000..394378c --- /dev/null +++ b/src/lib.tests.with_scale_round.rs @@ -0,0 +1,113 @@ +// Test BigDecimal::with_scale_round + +macro_rules! impl_test { + ( name=$($name:expr)*; $scale:literal : $mode:ident => $ex:literal ) => { + paste! { + #[test] + fn [< $($name)* _rounding_ $mode >]() { + let bigdecimal = test_input(); + let result = bigdecimal.with_scale_round($scale as i64, RoundingMode::$mode); + let expected = BigDecimal::from_str($ex).unwrap(); + assert_eq!(result, expected); + assert_eq!(result.int_val, expected.int_val); + assert_eq!(result.scale, $scale); + } + } + }; + ( -$scale:literal $( : $($modes:ident),+ => $ex:literal )+ ) => { + $( $( impl_test!(name=scale_neg_ $scale; -$scale : $modes => $ex); )* )* + }; + ( $scale:literal $( : $($modes:ident),+ => $ex:literal )+ ) => { + $( $( impl_test!(name=scale_ $scale; $scale : $modes => $ex); )* )* + }; +} + + +mod case_3009788271450eNeg9 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::from_str("3009.788271450").unwrap() + } + + impl_test!(10 : Up, Down => "3009.7882714500"); + impl_test!(9 : Up, Down => "3009.788271450"); + impl_test!(8 : Up, Down, HalfEven => "3009.78827145"); + + impl_test!(7 : Up, Ceiling, HalfUp => "3009.7882715" + : Down, Floor, HalfDown, HalfEven => "3009.7882714"); + + impl_test!(4 : Up, Ceiling, HalfUp, HalfDown, HalfEven => "3009.7883" + : Down, Floor => "3009.7882"); + + impl_test!(2 : Up => "3009.79" + : Down => "3009.78"); + + impl_test!(1 : Up => "3009.8" + : Down => "3009.7"); + + impl_test!(0 : Up => "3010" + : Down => "3009"); + + impl_test!( -1 : Up => "301e1"); + impl_test!( -2 : Up => "31e2"); + impl_test!( -3 : Up => "4e3"); + impl_test!( -4 : Up => "1e4" ); + impl_test!( -5 : Up => "1e5" : Down => "0"); + impl_test!( -20 : Up => "1e20" : Down => "0"); +} + +mod case_neg_636652287787259 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::from_str("-636652287787259").unwrap() + } + + impl_test!(1 : Up, Down => "-636652287787259.0"); + impl_test!(0 : Up, Down => "-636652287787259"); + impl_test!(-1 : Up => "-63665228778726e1" + : Down => "-63665228778725e1"); + impl_test!(-12 : Up => "-637e12" + : Down => "-636e12"); +} + +mod case_99999999999999999999999eNeg4 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::from_str("99999999999999999999999e-4").unwrap() + } + + impl_test!(4 : Up => "9999999999999999999.9999"); + impl_test!(3 : Up => "10000000000000000000.000" + : Down => "9999999999999999999.999"); + impl_test!(-3 : Up => "10000000000000000e3" + : Down => "9999999999999999e3"); +} + +mod case_369708962060657eNeg30 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::from_str("369708962060657E-30").unwrap() + } + + impl_test!(4 : Up => "1e-4"); + impl_test!(20 : Up => "36971e-20" + : Down => "36970e-20"); +} + +mod case_682829560896740e30 { + use super::*; + + fn test_input() -> BigDecimal { + BigDecimal::from_str("682829560896740e30").unwrap() + } + + impl_test!(4 : Up => "682829560896740000000000000000000000000000000.0000"); + impl_test!(0 : Up => "682829560896740000000000000000000000000000000"); + impl_test!(-35 : Up => "6828295609e35"); + impl_test!(-36 : Up => "682829561e36"); + impl_test!(-100 : Up => "1e100"); +} diff --git a/src/parsing.rs b/src/parsing.rs new file mode 100644 index 0000000..8bd917b --- /dev/null +++ b/src/parsing.rs @@ -0,0 +1,199 @@ +//! Routines for parsing values into BigDecimals + +use super::{BigDecimal, ParseBigDecimalError}; + +use stdlib::cmp::{self, Ordering}; + +use num_bigint::{BigInt, BigUint, Sign}; +use num_traits::Zero; + + +/// Try creating bigdecimal from f32 +/// +/// Non "normal" values will return Error case +/// +pub(crate) fn try_parse_from_f32(n: f32) -> Result { + use stdlib::num::FpCategory::*; + match n.classify() { + Nan => Err(ParseBigDecimalError::Other("NAN".into())), + Infinite => Err(ParseBigDecimalError::Other("Infinite".into())), + Subnormal => Err(ParseBigDecimalError::Other("Subnormal".into())), + Normal | Zero => Ok(parse_from_f32(n)), + } +} + + +/// Return mantissa, exponent, and sign of given floating point number +/// +/// ```math +/// f = frac * 2^pow +/// ``` +/// +fn split_f32_into_parts(f: f32) -> (u32, i64, Sign) { + let bits = f.to_bits(); + let frac = (bits & ((1 << 23) - 1)) + (1 << 23); + let exp = (bits >> 23) & 0xFF; + + let pow = exp as i64 - 127 - 23; + + let sign_bit = bits & (1 << 31); + let sign = if sign_bit == 0 { + Sign::Plus + } else { + Sign::Minus + }; + + (frac, pow, sign) +} + + +/// Create bigdecimal from f32 +/// +/// Non "normal" values is undefined behavior +/// +pub(crate) fn parse_from_f32(n: f32) -> BigDecimal { + let bits = n.to_bits(); + + if (bits << 1) == 0 { + return Zero::zero(); + } + + // n = frac * 2^pow + let (frac, pow, sign) = split_f32_into_parts(n); + + let result; + let scale; + match pow.cmp(&0) { + Ordering::Equal => { + result = BigUint::from(frac); + scale = 0; + } + Ordering::Less => { + let trailing_zeros = cmp::min(frac.trailing_zeros(), -pow as u32); + + let reduced_frac = frac >> trailing_zeros; + let reduced_pow = pow + trailing_zeros as i64; + debug_assert!(reduced_pow <= 0); + + let shift = BigUint::from(5u8).pow(-reduced_pow as u32); + + result = reduced_frac * shift; + scale = -reduced_pow; + } + Ordering::Greater => { + let shift = BigUint::from(2u8).pow(pow.abs() as u32); + + result = frac * shift; + scale = 0; + } + } + + BigDecimal { + int_val: BigInt::from_biguint(sign, result), + scale: scale, + } +} + +#[cfg(test)] +#[allow(non_snake_case)] +mod test_parse_from_f32 { + use super::*; + + include!("parsing.tests.parse_from_f32.rs"); +} + + +/// Try creating bigdecimal from f64 +/// +/// Non "normal" values will return Error case +/// +pub(crate) fn try_parse_from_f64(n: f64) -> Result { + use stdlib::num::FpCategory::*; + match n.classify() { + Nan => Err(ParseBigDecimalError::Other("NAN".into())), + Infinite => Err(ParseBigDecimalError::Other("Infinite".into())), + Subnormal => Err(ParseBigDecimalError::Other("Subnormal".into())), + Normal | Zero => Ok(parse_from_f64(n)), + } +} + + +/// Return mantissa, exponent, and sign of given floating point number +/// +/// ```math +/// f = frac * 2^pow +/// ``` +/// +fn split_f64_into_parts(f: f64) -> (u64, i64, Sign) { + let bits = f.to_bits(); + let frac = (bits & ((1 << 52) - 1)) + (1 << 52); + let exp = (bits >> 52) & 0x7FF; + + let pow = exp as i64 - 1023 - 52; + + let sign_bit = bits & (1 << 63); + let sign = if sign_bit == 0 { + Sign::Plus + } else { + Sign::Minus + }; + + (frac, pow, sign) +} + + +/// Create bigdecimal from f64 +/// +/// Non "normal" values is undefined behavior +/// +pub(crate) fn parse_from_f64(n: f64) -> BigDecimal { + let bits = n.to_bits(); + + // shift right by 1 bit to handle -0.0 + if (bits << 1) == 0 { + return Zero::zero(); + } + + // n = frac * 2^pow + let (frac, pow, sign) = split_f64_into_parts(n); + debug_assert!(frac > 0); + + let result; + let scale; + match pow.cmp(&0) { + Ordering::Equal => { + result = BigUint::from(frac); + scale = 0; + } + Ordering::Less => { + let trailing_zeros = cmp::min(frac.trailing_zeros(), -pow as u32); + + let reduced_frac = frac >> trailing_zeros; + let reduced_pow = pow + trailing_zeros as i64; + debug_assert!(reduced_pow <= 0); + + let shift = BigUint::from(5u8).pow(-reduced_pow as u32); + + result = reduced_frac * shift; + scale = -reduced_pow; + } + Ordering::Greater => { + let shift = BigUint::from(2u8).pow(pow as u32); + result = frac * shift; + scale = 0; + } + } + + BigDecimal { + int_val: BigInt::from_biguint(sign, result), + scale: scale, + } +} + +#[cfg(test)] +#[allow(non_snake_case)] +mod test_parse_from_f64 { + use super::*; + + include!("parsing.tests.parse_from_f64.rs"); +} diff --git a/src/parsing.tests.parse_from_f32.rs b/src/parsing.tests.parse_from_f32.rs new file mode 100644 index 0000000..ab9fd29 --- /dev/null +++ b/src/parsing.tests.parse_from_f32.rs @@ -0,0 +1,110 @@ +// tests for function bigdecimal::parsing::parse_from_f32 + +use paste::paste; + +use stdlib::f32; + +macro_rules! impl_test { + ($name:ident : $input:literal == $expected:literal) => { + paste! { + #[test] + fn [< case $name >]() { + let n = $input as f32; + let d = parse_from_f32(n); + assert_eq!(d, $expected.parse().unwrap()); + } + + #[test] + fn [< case_neg $name >]() { + let n = -($input as f32); + let d = parse_from_f32(n); + assert_eq!(d, concat!("-", $expected).parse().unwrap()); + } + } + }; +} + + +impl_test!(_0 : 0.0 == "0"); +impl_test!(_1 : 1.0 == "1"); +impl_test!(_5en1 : 0.5 == "0.5"); +impl_test!(_25en2 : 0.25 == "0.25"); +impl_test!(_50 : 50. == "50"); +impl_test!(_1en3 : 0.001 == "0.001000000047497451305389404296875"); +impl_test!(_033203125en8 : 0.033203125 == "0.033203125"); + +impl_test!(_45En1 : 4.5 == "4.5"); +impl_test!(_15625En5 : 0.15625 == "0.15625"); +impl_test!(_1192092896En7 : 1.192092896e-7 == "1.1920928955078125E-7"); +impl_test!(_1401757440 : 1401757440. == "1401757440"); +impl_test!(_215092En1 : 21509.2 == "21509.19921875"); +impl_test!(_2289620000 : 2289620000.0 == "2289619968"); +impl_test!(_10000000 : 10000000. == "10000000"); +impl_test!(_1en05 : 1e-5 == "0.00000999999974737875163555145263671875"); +impl_test!(_1en1 : 1e-1 == "0.100000001490116119384765625"); +impl_test!(_2en1 : 2e-1 == "0.20000000298023223876953125"); +impl_test!(_80000197 : 80000197e0 == "80000200"); +impl_test!(_23283064En16 : 2.3283064e-10 == "0.00000000023283064365386962890625"); +impl_test!(_14693861798803098En17 : 0.14693861798803098 == "0.146938621997833251953125"); +impl_test!(_1e20 : 1e20 == "100000002004087734272"); +impl_test!(_1e30 : 1e30 == "1000000015047466219876688855040"); +impl_test!(_1e38 : 1e38 == "99999996802856924650656260769173209088"); +impl_test!(_317e36 : 317e36 == "317000006395220278118691742155288870912"); +impl_test!(_23509889819en48 : 2.3509889819e-38 == "2.35098898190426788090088725919040801362055736959656341832065776397049129686767088287524529732763767242431640625E-38"); +impl_test!(_235098744048en49 : 2.35098744048e-38 == "2.350987440475957123602109243087866394712812961308427354153308831195379018097479928428583662025630474090576171875E-38"); +impl_test!(_6_99999952316 : 6.99999952316 == "6.999999523162841796875"); +impl_test!(_317en40 : 317e-40 == "3.1700000098946435501119816090716154772221806896649747100732700841687651538425285480116144753992557525634765625E-38"); +impl_test!(_4294967295 : 4294967295. == "4294967296"); +impl_test!(_158456325029e18 : 1.58456325029e+29 == "158456325028528675187087900672"); + + +#[test] +fn case_f32_min() { + let n = f32::MIN; + let d = parse_from_f32(n); + assert_eq!(d, "-340282346638528859811704183484516925440".parse().unwrap()); +} + +#[test] +fn case_f32_max() { + let n = f32::MAX; + let d = parse_from_f32(n); + assert_eq!(d, "340282346638528859811704183484516925440".parse().unwrap()); +} + +#[test] +fn case_f32_epsilon() { + let n = f32::EPSILON; + let d = parse_from_f32(n); + assert_eq!(d, "1.1920928955078125E-7".parse().unwrap()); +} + +#[test] +fn case_f32_pi() { + let n = f32::consts::PI; + let d = parse_from_f32(n); + assert_eq!(d, "3.1415927410125732421875".parse().unwrap()); +} + +#[test] +fn case_nan() { + let n = f32::from_bits(0b01111111110000000000000000000000); + assert!(n.is_nan()); + + let d = parse_from_f32(n); + assert_eq!(d, "510423550381407695195061911147652317184".parse().unwrap()); +} + +#[test] +fn case_try_from_nan() { + let n = f32::NAN; + let d = try_parse_from_f32(n); + assert!(d.is_err()); +} + +#[test] +fn case_try_from_infinity() { + let n = f32::INFINITY; + let d = try_parse_from_f32(n); + assert!(d.is_err()); +} diff --git a/src/parsing.tests.parse_from_f64.rs b/src/parsing.tests.parse_from_f64.rs new file mode 100644 index 0000000..5f8c195 --- /dev/null +++ b/src/parsing.tests.parse_from_f64.rs @@ -0,0 +1,103 @@ +// tests for function bigdecimal::parsing::parse_from_f64 + +use paste::paste; + +use stdlib::f64; + +macro_rules! impl_test { + ($input:literal == $expected:literal) => { + paste! { impl_test!( [< "_" $input >] : $input == $expected); } + }; + ($name:ident : bits:$input:literal => $expected:literal) => { + impl_test!($name : f64::from_bits($input) => $expected); + }; + ($name:ident : $input:literal == $expected:literal) => { + impl_test!($name : ($input as f64) => $expected); + }; + ($name:ident : $input:expr => $expected:literal) => { + paste! { + #[test] + fn [< case $name >]() { + let n = $input; + let d = parse_from_f64(n); + assert_eq!(d, $expected.parse().unwrap()); + } + + #[test] + fn [< case_neg $name >]() { + let n = f64::from_bits($input.to_bits() | (1<<63)); + let d = parse_from_f64(n); + assert_eq!(d, concat!("-", $expected).parse().unwrap()); + } + } + }; +} + +impl_test!(_0 : 0.0 == "0"); +impl_test!(_1 : 1.0 == "1"); +impl_test!(_2 : 2.0 == "2"); +impl_test!(_3 : 3.0 == "3"); +impl_test!(_5en1 : 0.5 == "0.5"); +impl_test!(_25en2 : 0.25 == "0.25"); +impl_test!(_1en1 : 0.1 == "0.1000000000000000055511151231257827021181583404541015625"); +impl_test!(_1over3 : 0.333333333333333333333333333333 == "0.333333333333333314829616256247390992939472198486328125"); +impl_test!(_pi : 3.141592653589793 == "3.141592653589793115997963468544185161590576171875"); +impl_test!(_near_3 : 3.0000000000000004 == "3.000000000000000444089209850062616169452667236328125"); +impl_test!(_8eneg306 : 8.544283616667655e-306 == "8.5442836166676545758745469881475846986178991076220674838778719735182619591847930738097459423424470941335996703553180065389909675214026779902482660710563190540056652827644969523715287333767167538014707594736533997824798692690142890189753467148541192574394234161821394612038920127719106177776787375705338074667624093006332620080979623387970617655687653904110103913103933178304212511707769987213793880764157458662751217010283883439888757033430556011326632895537144105152597427684695380215955244686097497705226475608085097617996058799189036784865947060736971859470127760066696392182317083388979882704968230500619384728741377732016919538675848783600526390429792978252568964346334556191024880163233082812954995600973750951114861484914086986464099027216434478759765625e-306"); +impl_test!(_8e306 : 3e300 == "3000000000000000157514280765613260746113405743324477464747562346535407373966724587359114125241343592131113331498651634530827569706081291726934376554360120948545161602779727411213490701384364270178106859704912399835243357116902922640223958228340427483737776366460170528514347008416589160596378201620480"); + +impl_test!(_50 : 50. == "50"); +impl_test!(_nanbits : bits:0b_0_11111111111_1000000000000000000000000000000000000000000000000001 => "269653970229347426076201969312749943170150807578117307259170330445749843759196293443300553362892619730839480672521111823337121537071529813188030913831084401350087805833926634314566788423582671529934053315387252306324360914392174188827078768228648633522131134987762597502339006422840407304422939101316534763520"); +impl_test!(_3105036184601418e246 : bits:0b_0_11100000000_0000000000000000000000000000000000000000000000000000 => "3105036184601417870297958976925005110513772034233393222278104076052101905372753772661756817657292955900975461394262146412343160088229628782888574550082362278408909952041699811100530571263196889650525998387432937501785693707632115712"); + + +#[test] +fn case_f64_min() { + let n = f64::MIN; + let d = parse_from_f64(n); + assert_eq!(d, "-179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368".parse().unwrap()); +} + +#[test] +fn case_f64_max() { + let n = f64::MAX; + let d = parse_from_f64(n); + assert_eq!(d, "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368".parse().unwrap()); +} + +#[test] +fn case_f64_epsilon() { + let n = f64::EPSILON; + let d = parse_from_f64(n); + assert_eq!(d, "2.220446049250313080847263336181640625e-16".parse().unwrap()); +} + +#[test] +fn case_f64_pi() { + let n = f64::consts::PI; + let d = parse_from_f64(n); + assert_eq!(d, "3.141592653589793115997963468544185161590576171875".parse().unwrap()); +} + +#[test] +fn case_nan() { + let n = f64::from_bits(0b0_11111111111_1000000000000000000000000000000000000000000000000000); + assert!(n.is_nan()); + + let d = parse_from_f64(n); + assert_eq!(d, "269653970229347386159395778618353710042696546841345985910145121736599013708251444699062715983611304031680170819807090036488184653221624933739271145959211186566651840137298227914453329401869141179179624428127508653257226023513694322210869665811240855745025766026879447359920868907719574457253034494436336205824".parse().unwrap()); +} + +#[test] +fn case_try_from_nan() { + let n = f64::NAN; + let d = try_parse_from_f64(n); + assert!(d.is_err()); +} + +#[test] +fn case_try_from_infinity() { + let n = f64::INFINITY; + let d = try_parse_from_f64(n); + assert!(d.is_err()); +} diff --git a/src/rounding.rs b/src/rounding.rs new file mode 100644 index 0000000..a1053d5 --- /dev/null +++ b/src/rounding.rs @@ -0,0 +1,465 @@ +//! Rounding structures and subroutines + +use crate::Sign; +use stdlib; + +/// Determines how to calculate the last digit of the number +/// +/// Default rounding mode is HalfUp +/// +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub enum RoundingMode { + /// Always round away from zero + /// + /// + /// * 5.5 → 6.0 + /// * 2.5 → 3.0 + /// * 1.6 → 2.0 + /// * 1.1 → 2.0 + /// * -1.1 → -2.0 + /// * -1.6 → -2.0 + /// * -2.5 → -3.0 + /// * -5.5 → -6.0 + Up, + + /// Always round towards zero + /// + /// * 5.5 → 5.0 + /// * 2.5 → 2.0 + /// * 1.6 → 1.0 + /// * 1.1 → 1.0 + /// * -1.1 → -1.0 + /// * -1.6 → -1.0 + /// * -2.5 → -2.0 + /// * -5.5 → -5.0 + Down, + + /// Towards +∞ + /// + /// * 5.5 → 6.0 + /// * 2.5 → 3.0 + /// * 1.6 → 2.0 + /// * 1.1 → 2.0 + /// * -1.1 → -1.0 + /// * -1.6 → -1.0 + /// * -2.5 → -2.0 + /// * -5.5 → -5.0 + Ceiling, + + /// Towards -∞ + /// + /// * 5.5 → 5.0 + /// * 2.5 → 2.0 + /// * 1.6 → 1.0 + /// * 1.1 → 1.0 + /// * -1.1 → -2.0 + /// * -1.6 → -2.0 + /// * -2.5 → -3.0 + /// * -5.5 → -6.0 + Floor, + + /// Round to 'nearest neighbor', or up if ending decimal is 5 + /// + /// * 5.5 → 6.0 + /// * 2.5 → 3.0 + /// * 1.6 → 2.0 + /// * 1.1 → 1.0 + /// * -1.1 → -1.0 + /// * -1.6 → -2.0 + /// * -2.5 → -3.0 + /// * -5.5 → -6.0 + HalfUp, + + /// Round to 'nearest neighbor', or down if ending decimal is 5 + /// + /// * 5.5 → 5.0 + /// * 2.5 → 2.0 + /// * 1.6 → 2.0 + /// * 1.1 → 1.0 + /// * -1.1 → -1.0 + /// * -1.6 → -2.0 + /// * -2.5 → -2.0 + /// * -5.5 → -5.0 + HalfDown, + + /// Round to 'nearest neighbor', if equidistant, round towards + /// nearest even digit + /// + /// * 5.5 → 6.0 + /// * 2.5 → 2.0 + /// * 1.6 → 2.0 + /// * 1.1 → 1.0 + /// * -1.1 → -1.0 + /// * -1.6 → -2.0 + /// * -2.5 → -2.0 + /// * -5.5 → -6.0 + /// + HalfEven, +} + + +impl RoundingMode { + /// Perform the rounding operation + /// + /// Parameters + /// ---------- + /// * sign (Sign) - Sign of the number to be rounded + /// * pair (u8, u8) - The two digits in question to be rounded. + /// i.e. to round 0.345 to two places, you would pass (4, 5). + /// As decimal digits, they + /// must be less than ten! + /// * trailing_zeros (bool) - True if all digits after the pair are zero. + /// This has an effect if the right hand digit is 0 or 5. + /// + /// Returns + /// ------- + /// Returns the first number of the pair, rounded. The sign is not preserved. + /// + /// Examples + /// -------- + /// - To round 2341, pass in `Plus, (4, 1), true` → get 4 or 5 depending on scheme + /// - To round -0.1051, to two places: `Minus, (0, 5), false` → returns either 0 or 1 + /// - To round -0.1, pass in `true, (0, 1)` → returns either 0 or 1 + /// + /// Calculation of pair of digits from full number, and the replacement of that number + /// should be handled separately + /// + pub fn round_pair(&self, sign: Sign, pair: (u8, u8), trailing_zeros: bool) -> u8 { + use self::RoundingMode::*; + use stdlib::cmp::Ordering::*; + + let (lhs, rhs) = pair; + // if all zero after digit, never round + if rhs == 0 && trailing_zeros { + return lhs; + } + let up = lhs + 1; + let down = lhs; + match (*self, rhs.cmp(&5)) { + (Up, _) => up, + (Down, _) => down, + (Floor, _) => if sign == Sign::Minus { up } else { down }, + (Ceiling, _) => if sign == Sign::Minus { down } else { up }, + (_, Less) => down, + (_, Greater) => up, + (_, Equal) if !trailing_zeros => up, + (HalfUp, Equal) => up, + (HalfDown, Equal) => down, + (HalfEven, Equal) => if lhs % 2 == 0 { down } else { up }, + } + } + + /// Round value at particular digit, returning replacement digit + /// + /// Parameters + /// ---------- + /// * at_digit (NonZeroU8) - 0-based index of digit at which to round. + /// 0 would be the first digit, and would + /// + /// * sign (Sign) - Sign of the number to be rounded + /// * value (u32) - The number containing digits to be rounded. + /// * trailing_zeros (bool) - True if all digits after the value are zero. + /// + /// Returns + /// ------- + /// Returns the first number of the pair, rounded. The sign is not preserved. + /// + /// Examples + /// -------- + /// - To round 823418, at digit-index 3: `3, Plus, 823418, true` → 823000 or 824000, depending on scheme + /// - To round -100205, at digit-index 1: `1, Minus, 100205, true` → 100200 or 100210 + /// + /// 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 { + let shift = 10u32.pow(at_digit.get() as u32 - 1); + let splitter = shift * 10; + + // split 'value' into high and low + let (top, bottom) = num_integer::div_rem(value, splitter); + let lhs = (top % 10) as u8; + let (rhs, remainder) = num_integer::div_rem(bottom, shift); + let pair = (lhs, rhs as u8); + let rounded = self.round_pair(sign, pair, trailing_zeros && remainder == 0); + + // replace low digit with rounded value + let full = top - lhs as u32 + rounded as u32; + + // shift rounded value back to position + full * splitter + } +} + + +#[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); + } +} diff --git a/src/with_std.rs b/src/with_std.rs new file mode 100644 index 0000000..4cddf7c --- /dev/null +++ b/src/with_std.rs @@ -0,0 +1,26 @@ + +// Wrap std:: modules in namespace +#[allow(unused_imports)] +mod stdlib { + + pub use std::{ + cmp, + convert, + default, + fmt, + hash, + mem, + num, + ops, + iter, + str, + string, + i8, + f32, + f64, + }; + + + #[cfg(test)] + pub use std::collections::hash_map::DefaultHasher; +} diff --git a/src/without_std.rs b/src/without_std.rs new file mode 100644 index 0000000..8e35874 --- /dev/null +++ b/src/without_std.rs @@ -0,0 +1,37 @@ +#[allow(unused_imports)] +#[macro_use] +extern crate alloc; + +#[cfg(test)] +extern crate siphasher; + +// Without this import we get the following error: +// error[E0599]: no method naemed `powi` found for type `f64` in the current scope +#[allow(unused_imports)] +use num_traits::float::FloatCore; + +// Wrap core:: modules in namespace +#[allow(unused_imports)] +mod stdlib { + + pub use core::{ + cmp, + convert, + default, + fmt, + hash, + mem, + num, + ops, + iter, + str, + i8, + f32, + f64, + }; + + #[cfg(test)] + pub use siphasher::sip::SipHasher as DefaultHasher; + + pub use alloc::string; +}