Skip to content

Commit

Permalink
v0.4.4
Browse files Browse the repository at this point in the history
  • Loading branch information
akubera committed Jun 15, 2024
2 parents f84ba98 + 86cecb4 commit 609cd3d
Show file tree
Hide file tree
Showing 13 changed files with 1,588 additions and 287 deletions.
12 changes: 12 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ workflows:
name: "lint-test-build:1.56"
release: true
version: "1.56"
pre-steps:
- checkout
- run:
command: cargo generate-lockfile

- lint-check

Expand Down Expand Up @@ -159,6 +163,14 @@ workflows:
name: build-and-test:serde+no_std
rust-features: "--no-default-features --features='serde'"

- build-and-test:
name: build-and-test:serde_json
rust-features: "--features='serde-json'"

- build-and-test:
name: build-and-test:serde_json+no_std
rust-features: "--features='serde-json' --no-default-features"

- cargo-semver-check:
requires:
- build-and-test:latest:serde
Expand Down
35 changes: 35 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,29 @@ cargo:test:serde:
<<: *script-cargo-test
script:
- cargo test --features=serde --all-targets
- cargo test --features=serde --no-default-features

cargo:build:serde-json:
stage: build
image: akubera/rust:stable
needs:
- cargo:check
variables:
RUST_CACHE_KEY: "stable+serde-json"
<<: *script-cargo-build
script:
- cargo build --features=serde-json --all-targets

cargo:test:serde-json:
stage: test
image: akubera/rust:stable
needs:
- "cargo:build:serde-json"
variables:
RUST_CACHE_KEY: "stable+serde-json"
<<: *script-cargo-test
script:
- cargo test --features=serde-json --all-targets


cargo:build-nightly:
Expand Down Expand Up @@ -265,6 +288,18 @@ cargo:test-1.43:
RUST_CACHE_KEY: "1.43"
<<: *script-cargo-test

cargo:test-1.43:serde:
stage: test
needs:
- "cargo:build-1.43"
image: "akubera/rust-kcov:1.43.1-buster"
variables:
RUST_CACHE_KEY: "1.43-serde"
<<: *script-cargo-test
script:
- cargo test --features=serde --all-targets
- cargo test --features=serde --no-default-features


cargo:check-1.70:
stage: check
Expand Down
11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bigdecimal"
version = "0.4.3"
version = "0.4.4"
authors = ["Andrew Kubera"]
description = "Arbitrary precision decimal numbers"
documentation = "https://docs.rs/bigdecimal"
Expand All @@ -21,10 +21,12 @@ bench = false

[dependencies]
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 }
num-bigint = { version = ">=0.3.0,<0.4.5", default-features = false }
num-integer = { version = "^0.1", default-features = false }
num-traits = { version = ">0.2.0,<0.2.19", default-features = false }
serde = { version = "1.0", optional = true, default-features = false }
# Allow direct parsing of JSON floats, for full arbitrary precision
serde_json = { version = "1.0", optional = true, default-features = false, features = ["alloc", "arbitrary_precision"]}

[dev-dependencies]
paste = "1"
Expand All @@ -44,6 +46,7 @@ autocfg = "1"

[features]
default = ["std"]
serde-json = ["serde/derive", "serde_json"]
string-only = []
std = ["num-bigint/std", "num-integer/std", "num-traits/std"]

Expand Down
142 changes: 137 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,147 @@ this code will print
sqrt(2) = 1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573
```

### Serialization

If you are passing BigDecimals between systems, be sure to use a serialization format
which explicitly supports decimal numbers and does not require transformations to
floating-point binary numbers, or there will be information loss.

Text formats like JSON should work ok as long as the receiver will also parse
numbers as decimals so complete precision is kept accurate.
Typically JSON-parsing implementations do not do this by default, and need special
configuration.

Binary formats like msgpack may expect/require representing numbers as 64-bit IEEE-754
floating-point, and will likely lose precision by default unless you explicitly format
the decimal as a string, bytes, or some custom structure.

By default, this will serialize the decimal _as a string_.

To use `serde_json` with this crate it is recommended to enable the `serde-json` feature
(note `serde -dash- json` , not `serde_json`) and that will add support for
serializing & deserializing values to BigDecimal.
By default it will parse numbers and strings using normal conventions.
If you want to serialize to a number, rather than a string, you can use the
`serde(with)` annotation as shown in the following example:

```toml
[dependencies]
bigdecimal = { version = "0.4", features = [ "serde-json" ] } # '-' not '_'
```

```rust
use bigdecimal::BigDecimal;
use serde::*;
use serde_json;

#[derive(Debug,Serialize,Deserialize)]
struct MyStruct {
name: String,
// this will be witten to json as string
value: BigDecimal,
// this will be witten to json as number
#[serde(with = "bigdecimal::serde::json_num")]
number: BigDecimal,
}

fn main() {
let json_src = r#"
{ "name": "foo", "value": 1234567e-3, "number": 3.14159 }
"#;

let my_struct: MyStruct = serde_json::from_str(&json_src).unwrap();
dbg!(my_struct);
// MyStruct { name: "foo", value: BigDecimal("1234.567"), BigDecimal("3.1459") }

println!("{}", serde_json::to_string(&my_struct));
// {"name":"foo","value":"1234.567","number":3.1459}
}
```

If you have suggestions for improving serialization, please bring them
to the Zulip chat.

### Formatting

Until a more sophisticated formatting solution is implemented (currently work in progress),
we are restricted to Rust's `fmt::Display` formatting options.
This is how this crate formats BigDecimals:

- `{}` - Default Display
- Format as "human readable" number
- "Small" fractional numbers (close to zero) are printed in scientific notation
- Number is considered "small" by number of leading zeros exceeding a threshold
- Configurable by the compile-time environment variable:
`RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD`
- Default 5
- Example: `1.23e-3` will print as `0.00123` but `1.23e-10` will be `1.23E-10`
- Trailing zeros will be added to "small" integers, avoiding scientific notation
- May appear to have more precision than they do
- Example: decimal `1e1` would be rendered as `10`
- The threshold for "small" is configured by compile-time environment variable:
`RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD`
- Default 15
- `1e15` => `1000000000000000`
- Large integers (e.g. `1e50000000`) will print in scientific notation, not
a 1 followed by fifty million zeros
- All other numbers are printed in standard decimal notation

- `{:.<PREC>}` - Display with precision
- Format number with exactly `PREC` digits after the decimal place
- Numbers with fractional components will be rounded at precision point, or have
zeros padded to precision point
- Integers will have zeros padded to the precision point
- To prevent unreasonably sized output, a threshold limits the number
of padded zeros
- Greater than the default case, since specific precision was requested
- Configurable by the compile-time environment variable:
`RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING`
- Default 1000
- If digits exceed this threshold, they are printed without decimal-point,
suffixed with scale of the big decimal

- `{:e}` / `{:E}` - Exponential format
- Formats in scientific notation with either `e` or `E` as exponent delimiter
- Precision is kept exactly

- `{:.<PREC>e}` - formats in scientific notation, keeping number
- Number is rounded / zero padded until

- `{:?}` - Debug
- Shows internal representation of BigDecimal
- `123.456` => `BigDecimal(sign=Plus, scale=3, digits=[123456])`
- `-1e10000` => `BigDecimal(sign=Minus, scale=-10000, digits=[1])`

- `{:#?}` - Alternate Debug (used by `dbg!()`)
- Shows simple int+exponent string representation of BigDecimal
- `123.456` => `BigDecimal("123456e-3")`
- `-1e10000` => `BigDecimal("-1e10000")`

There is a [formatting-example](examples/formatting-example.rs) script in the
`examples/` directory that demonstrates the formatting options and comparison
with Rust's standard floating point Display.

It is recommended you include unit tests in your code to guarantee that future
versions of BigDecimal continue to format numbers the way you expect.
The rules above are not likely to change, but they are probably the only part
of this library that are relatively subjective, and could change behavior
without indication from the compiler.

Also, check for changes to the configuration environment variables, those
may change name until 1.0.

### Compile-Time Configuration

You can set a few default parameters at _compile-time_ via environment variables:

| Environment Variable | Default |
|-------------------------------------------------|------------|
| `RUST_BIGDECIMAL_DEFAULT_PRECISION` | 100 |
| `RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE` | `HalfEven` |
| `RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD` | 5 |
| Environment Variable | Default |
|----------------------------------------------------|------------|
| `RUST_BIGDECIMAL_DEFAULT_PRECISION` | 100 |
| `RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE` | `HalfEven` |
| `RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD` | 5 |
| `RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD` | 15 |
| `RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING` | 1000 |

These allow setting the default [Context] fields globally without incurring a runtime lookup,
or having to pass Context parameters through all calculations.
Expand Down Expand Up @@ -137,6 +268,7 @@ $ cargo run
```

> [!NOTE]
>
> These are **compile time** environment variables, and the BigDecimal
> library is not configurable at **runtime** via environment variable, or
> any kind of global variables, by default.
Expand Down
73 changes: 59 additions & 14 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@

use std::env;
use std::path::{Path, PathBuf};

// configuration defaults
const DEFAULT_PRECISION: &str = "100";
const DEFAULT_ROUNDING_MODE: &str = "HalfEven";
const FMT_EXPONENTIAL_LOWER_THRESHOLD: &str = "5";
const FMT_EXPONENTIAL_UPPER_THRESHOLD: &str = "15";
const FMT_MAX_INTEGER_PADDING: &str = "1000";

const SERDE_MAX_SCALE: &str = "150000";


fn main() {
let ac = autocfg::new();
Expand All @@ -17,14 +25,20 @@ fn main() {
write_default_precision_file(&outdir);
write_default_rounding_mode(&outdir);
write_exponential_format_threshold_file(&outdir);
write_max_serde_parsing_scale_limit(&outdir);
}

/// Loads the environment variable string or default
macro_rules! load_env {
($env:ident, $name:literal, $default:ident) => {{
println!("cargo:rerun-if-env-changed={}", $name);
$env::var($name).unwrap_or_else(|_| $default.to_owned())
}};
}

/// Create default_precision.rs, containing definition of constant DEFAULT_PRECISION loaded in src/lib.rs
fn write_default_precision_file(outdir: &Path) {
let env_var = env::var("RUST_BIGDECIMAL_DEFAULT_PRECISION").unwrap_or_else(|_| "100".to_owned());
println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_DEFAULT_PRECISION");

let env_var = load_env!(env, "RUST_BIGDECIMAL_DEFAULT_PRECISION", DEFAULT_PRECISION);
let rust_file_path = outdir.join("default_precision.rs");

let default_prec: u32 = env_var
Expand All @@ -39,8 +53,7 @@ fn write_default_precision_file(outdir: &Path) {

/// Create default_rounding_mode.rs, using value of RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE environment variable
fn write_default_rounding_mode(outdir: &Path) {
let rounding_mode_name = env::var("RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE").unwrap_or_else(|_| "HalfEven".to_owned());
println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE");
let rounding_mode_name = load_env!(env, "RUST_BIGDECIMAL_DEFAULT_ROUNDING_MODE", DEFAULT_ROUNDING_MODE);

let rust_file_path = outdir.join("default_rounding_mode.rs");
let rust_file_contents = format!("const DEFAULT_ROUNDING_MODE: RoundingMode = RoundingMode::{};", rounding_mode_name);
Expand All @@ -50,17 +63,49 @@ fn write_default_rounding_mode(outdir: &Path) {

/// Create write_default_rounding_mode.rs, containing definition of constant EXPONENTIAL_FORMAT_THRESHOLD loaded in src/impl_fmt.rs
fn write_exponential_format_threshold_file(outdir: &Path) {
let env_var = env::var("RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD").unwrap_or_else(|_| "5".to_owned());
println!("cargo:rerun-if-env-changed=RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD");
let low_value = load_env!(env, "RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD", FMT_EXPONENTIAL_LOWER_THRESHOLD);
let high_value = load_env!(env, "RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD", FMT_EXPONENTIAL_UPPER_THRESHOLD);
let max_padding = load_env!(env, "RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING", FMT_MAX_INTEGER_PADDING);

let rust_file_path = outdir.join("exponential_format_threshold.rs");

let value: u32 = env_var
let low_value: u32 = low_value
.parse::<std::num::NonZeroU32>()
.expect("$RUST_BIGDECIMAL_EXPONENTIAL_FORMAT_THRESHOLD must be an integer > 0")
.expect("$RUST_BIGDECIMAL_FMT_EXPONENTIAL_LOWER_THRESHOLD must be an integer > 0")
.into();

let rust_file_contents = format!("const EXPONENTIAL_FORMAT_THRESHOLD: i64 = {};", value);
let high_value: u32 = high_value
.parse::<u32>()
.expect("$RUST_BIGDECIMAL_FMT_EXPONENTIAL_UPPER_THRESHOLD must be valid u32");

std::fs::write(rust_file_path, rust_file_contents).unwrap();
let max_padding: u32 = max_padding
.parse::<u32>()
.expect("$RUST_BIGDECIMAL_FMT_MAX_INTEGER_PADDING must be valid u32");

let rust_file_path = outdir.join("exponential_format_threshold.rs");

let rust_file_contents = [
format!("const EXPONENTIAL_FORMAT_LEADING_ZERO_THRESHOLD: usize = {};", low_value),
format!("const EXPONENTIAL_FORMAT_TRAILING_ZERO_THRESHOLD: usize = {};", high_value),
format!("const FMT_MAX_INTEGER_PADDING: usize = {};", max_padding),
];

std::fs::write(rust_file_path, rust_file_contents.join("\n")).unwrap();
}


/// Create write_default_rounding_mode.rs, containing definition of constant EXPONENTIAL_FORMAT_THRESHOLD loaded in src/impl_fmt.rs
fn write_max_serde_parsing_scale_limit(outdir: &Path) {
let scale_limit = load_env!(env, "RUST_BIGDECIMAL_SERDE_SCALE_LIMIT", SERDE_MAX_SCALE);

let scale_limit: u32 = scale_limit
.parse::<u32>()
.or_else(|e| if scale_limit.to_lowercase() == "none" { Ok(0) } else { Err(e) })
.expect("$RUST_BIGDECIMAL_SERDE_SCALE_LIMIT must be an integer");

let rust_file_path = outdir.join("serde_scale_limit.rs");

let rust_file_contents = [
format!("const SERDE_SCALE_LIMIT: i64 = {};", scale_limit),
];

std::fs::write(rust_file_path, rust_file_contents.join("\n")).unwrap();
}
Loading

0 comments on commit 609cd3d

Please sign in to comment.