human_format-1.2.1/.cargo_vcs_info.json0000644000000001360000000000100135150ustar { "git": { "sha1": "55f5b1fd69f88900a0aa5cd1d9c8e15486d83ff0" }, "path_in_vcs": "" }human_format-1.2.1/.editorconfig000064400000000000000000000003611046102023000147620ustar 00000000000000# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true human_format-1.2.1/.github/COPILOT_INSTRUCTIONS.md000064400000000000000000000053261046102023000173320ustar 00000000000000Copilot Instructions for human-format-rs ## Purpose This document gives guidance for automated copilots and contributors working on the human-format-rs repository. Follow these rules to make consistent, tested, and minimally-invasive changes. ## Repository layout - Cargo.toml: Rust crate manifest. Keep `edition = '2021'` and `resolver = "2"` unless instructed otherwise. - src/lib.rs: Primary library implementation. Be conservative when editing; prefer minimal, focused patches and run tests after edits. - tests/: Integration tests. Tests are organized into subfolders by concern: - tests/parsing/: parsing-related tests (SI, suffix parsing, micro sign, clamp behavior) - tests/formatting/: formatting-related tests (SI output, binary, forced suffix, time formatting, micro sign output) - tests/edge/: edge-case tests (parse errors, NaN/Inf, rounding, large-value handling, clamp behavior) ## Key conventions - API changes should be minimal and backwards-compatible where possible. When a breaking change is required, document it in CHANGELOG and README. - Prefer adding new small helper functions over extensive refactors unless requested. - Use `try_parse` for parsing and return Result; `parse()` is behind `panic_parse` feature. - `Scales::Time()` uses an explicit `HashMap` for unit multipliers; parsing and clamping should respect explicit_map first. ## Testing - Always run `cargo test` locally after making code or test changes; tests must pass. - New features need tests added to the appropriate `tests/*` folder. Keep tests focused and deterministic. - Avoid duplicating tests. If similar behavior should be validated from different angles (parse vs format), keep tests in separate logical folders. ## Documentation - Update README.md and changelog.md when behavior or public API changes. - Keep README examples brief and runnable; prefer `Formatter::new()` usage examples. ## Style and formatting - Follow existing code style in `src/lib.rs`. Keep changes small and consistent. - Avoid bulk reformatting of unrelated files. ## Safety and scope - Do not change tests or behavior that would invalidate user-facing assumptions without explicit approval. - When making non-trivial behavioral changes (e.g., clamping strategy), include tests and update README and changelog. ## PR checklist - Code compiles and `cargo test` passes. - Added or updated tests for new behaviors. - Updated `README.md` and `changelog.md` for user-facing changes. - Avoid changing unrelated files. ## Contact If unsure about changing behavior in `Scales::Time()` or the parsing/clamping semantics, ask the project maintainer before making breaking changes. human_format-1.2.1/.github/ISSUE_TEMPLATE.md000064400000000000000000000005651046102023000163600ustar 00000000000000## Summary of Issue or Feature _Please include a detailed description of the issue or feature._ _Mockups and examples should be used wherever relevant_ ### Reproduction Steps 1. Include dependency 2. ... ## Logs & Source Code _Please attach all relevant logs & source code_ _If possible, please provide an [MCVE](https://stackoverflow.com/help/mcve)_ human_format-1.2.1/.github/PULL_REQUEST_TEMPLATE.md000064400000000000000000000013501046102023000174450ustar 00000000000000## Overview This PR proposes... _provide a detailed explanation of the PR_ ## Test Cases & Validation Steps ## TODO - [ ] run `cargo test` and have 0 failed or skipped test cases - [ ] run `cargo fmt` and have 0 modified files - [ ] run `cargo clippy -- -Dwarnings` and have 0 issues reported - [ ] merge `develop` and validate that it no features are broken - [ ] document new public API - [ ] provide new test cases for any new/modified behavior - [ ] adhere to project standards & practices ## Further Enhancements _Here is an opportunity for you to provide some feedback on the PR to take it to the next level. These elements may become new issues in the backlog that may grow into their own PRs!_ human_format-1.2.1/.github/dependabot.yml000064400000000000000000000010321046102023000164710ustar 00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" human_format-1.2.1/.github/workflows/rust-beta.yml000064400000000000000000000027251046102023000203410ustar 00000000000000name: Rust - Beta/Nightly Checks on: push: branches: [develop, main] pull_request: branches: [develop, main] schedule: - cron: 0 0 * * 0 workflow_dispatch: env: CARGO_TERM_COLOR: always jobs: lint: name: Lint & Formatting Check strategy: matrix: toolchain: ["beta", "nightly"] runs-on: ubuntu-latest continue-on-error: true steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} - name: Install cargo components run: rustup component add rustfmt clippy - name: Format run: cargo fmt --check - name: Checker run: cargo check --verbose - name: Linter run: cargo clippy -- -Dwarnings test: name: Test strategy: matrix: toolchain: ["beta", "nightly"] runs-on: ubuntu-latest continue-on-error: true steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} - name: Run tests run: cargo test --verbose human_format-1.2.1/.github/workflows/rust.yml000064400000000000000000000046461046102023000174340ustar 00000000000000name: Rust on: push: branches: [develop, main] pull_request: branches: [develop, main] schedule: - cron: 0 0 * * 0 workflow_dispatch: env: CARGO_TERM_COLOR: always jobs: lint: name: Lint & Formatting Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Format run: cargo fmt --check - name: Checker run: cargo check --verbose - name: Linter run: cargo clippy -- -Dwarnings test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Run tests run: cargo test --verbose build: name: Build runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Build run: cargo build --verbose docs: name: Documentation runs-on: ubuntu-latest needs: [lint, test, build] steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Generate Docs run: cargo doc --examples --verbose # sonarcloud: # name: SonarCloud # runs-on: ubuntu-latest # needs: [build] # env: # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # steps: # - uses: actions/checkout@v4 # with: # fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis # - name: SonarCloud Scan # uses: SonarSource/sonarqube-scan-action@v4.2.1 # env: # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # SONAR_TOKEN: ${{ env.SONAR_TOKEN }} human_format-1.2.1/.gitignore000064400000000000000000000004631046102023000143000ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk human_format-1.2.1/CODEOWNERS000064400000000000000000000002751046102023000137040ustar 00000000000000# CODEOWNERS for human-format-rs # Format: [...] # Use GitHub usernames or team names. Adjust as needed. # Default owner for the repository /* @BobGneu human_format-1.2.1/Cargo.lock0000644000000002340000000000100114670ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "human_format" version = "1.2.1" human_format-1.2.1/Cargo.toml0000644000000031770000000000100115230ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" name = "human_format" version = "1.2.1" authors = ["Bob Chatman "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Rust Port of human-format from node, formatting numbers for us, while the machines are still at bay." homepage = "https://bobgneu.github.io/human-format-rs/" documentation = "https://docs.rs/human_format" readme = "README.md" keywords = [ "numbers", "formatting", "filesize", "human", "magnitude", ] categories = [ "value-formatting", "no-std", ] license = "MIT" repository = "https://github.com/BobGneu/human-format-rs" resolver = "2" [badges.maintenance] status = "passively-maintained" [features] panic_parse = [] [lib] name = "human_format" path = "src/lib.rs" [[test]] name = "demo" path = "tests/demo.rs" [[test]] name = "forced_suffix" path = "tests/forced_suffix.rs" [[test]] name = "micro_sign" path = "tests/micro_sign.rs" [[test]] name = "parse_clamp" path = "tests/parse_clamp.rs" [[test]] name = "si_roundtrip" path = "tests/si_roundtrip.rs" [[test]] name = "time_edgecases" path = "tests/time_edgecases.rs" human_format-1.2.1/Cargo.toml.orig000064400000000000000000000014351046102023000151770ustar 00000000000000[package] authors = ["Bob Chatman "] categories = ["value-formatting", "no-std"] description = "Rust Port of human-format from node, formatting numbers for us, while the machines are still at bay." documentation = "https://docs.rs/human_format" homepage = "https://bobgneu.github.io/human-format-rs/" keywords = ["numbers", "formatting", "filesize", "human", "magnitude"] license = "MIT" name = "human_format" readme = "README.md" repository = "https://github.com/BobGneu/human-format-rs" version = "1.2.1" edition = "2024" resolver = "2" [lib] name = "human_format" path = "src/lib.rs" [badges] maintenance = { status = "passively-maintained" } [features] # Opt-in feature to enable the panicking `parse()` convenience wrapper. panic_parse = [] human_format-1.2.1/LICENSE000064400000000000000000000020541046102023000133130ustar 00000000000000MIT License Copyright (c) 2018 Bob Chatman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. human_format-1.2.1/README.md000064400000000000000000000151571046102023000135750ustar 00000000000000# human_format [![Crates.io](https://img.shields.io/crates/v/human_format.svg)](https://crates.io/crates/human_format) [![Documentation](https://img.shields.io/badge/docs-rs-red.svg)](https://docs.rs/human_format) Rust Port of human-format from node, formatting numbers for us, while the machines are still at bay. | Main | Develop | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | [![Main](https://github.com/BobGneu/human-format-rs/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/BobGneu/human-format-rs/actions/workflows/rust.yml) | [![Develop](https://github.com/BobGneu/human-format-rs/actions/workflows/rust.yml/badge.svg?branch=develop)](https://github.com/BobGneu/human-format-rs/actions/workflows/rust.yml) | `human_format` is a small Rust library that makes numbers easier for people to read. It converts large or small numbers into short strings, like turning `1_000_000` into "1.00 M". ## What it does - Formats numbers with familiar suffixes: `k`, `M`, `G`, and so on. - Parses human-friendly strings back into numbers with `try_parse`. - Supports the newest SI prefixes (`R`, `Q`, `r`, `q`). - Accepts and (optionally) outputs the micro sign `µ`. - Lets you force a specific suffix for output (for example, always show values in `M`). - Includes a `Scales::Time()` option for common time units (seconds, minutes, hours, days, months, years, quarters, centuries, etc.). ## Quick start Add the crate: ```bash cargo add human_format ``` Use the formatter: ```rust use human_format::Formatter; // Default SI formatting let s = Formatter::new().format(1000.0); assert_eq!(s, "1.00 k"); // Control decimals let s = Formatter::new().with_decimals(1).format(1337.0); assert_eq!(s, "1.3 k"); ``` Use binary scales (base 1024): ```rust use human_format::{Formatter, Scales}; let s = Formatter::new().with_scales(Scales::Binary()).format(1024.0); assert_eq!(s, "1.00 ki"); ``` ## Parsing strings Convert a human-friendly string back to a `f64` with `try_parse`. ```rust use human_format::{Formatter, Scales}; let f = Formatter::new(); assert_eq!(f.try_parse("1.00 k").unwrap(), 1000.0); let mut fb = Formatter::new(); fb.with_scales(Scales::Binary()); assert_eq!(fb.try_parse("1.00 ki").unwrap(), 1024.0); ``` The parser accepts the micro sign `µ` as input. ## Force a suffix in output To force output to a certain suffix, use `with_suffix`. The value is scaled to match that suffix if possible. ```rust use human_format::Formatter; let mut f = Formatter::new(); f.with_suffix("M"); assert_eq!(f.format(100_000.0), "0.10 M"); ``` If the suffix is not valid for the current `Scales`, the formatter falls back to automatic selection. ## Micro sign output Use `with_micro_sign(true)` to show `µ` for micro values in the output. ```rust use human_format::Formatter; let mut f = Formatter::new(); f.with_micro_sign(true); assert_eq!(f.format(0.000001_f64), "1.00 µ"); ``` Parsing accepts both `u` and `µ`. ## Time scales `Scales::Time()` uses a set of explicit unit multipliers for time. It uses average values where needed (for example, the average year is 365.2425 days). Use `Scales::Time()` when you want time-aware formatting and parsing. ```rust use human_format::{Formatter, Scales}; let mut ft = Formatter::new(); ft.with_scales(Scales::Time()); // 90 seconds -> 1.50 m (minutes) assert_eq!(ft.format(90.0), "1.50 m"); // Quarters (qtr) parse as three-month periods assert!(ft.try_parse("1 qtr").is_ok()); ``` For more examples please consult [tests/demo.rs](https://github.com/BobGneu/human-format-rs/blob/develop/tests/demo.rs) ## Notes - Months and years are approximate here. For precise calendar math, use a date-time library. - Very large and very small numbers can lose precision when using `f64`. - The `with_suffix` method uses the same suffix strings that `try_parse` accepts. ### Suffix vs unit ambiguity Some short tokens can be ambiguous. For example, the single letter `m` can mean **milli** (a magnitude suffix) or **meter** (a measurement unit) depending on how you configure the `Formatter`: - `Formatter::new().with_suffix("m")` treats `m` as milli (scale). - `Formatter::new().with_units("m")` appends `m` as the units string (meter). To avoid ambiguity, prefer longer suffixes or specify units explicitly with `with_units`. ### Case sensitivity Suffix matching is case-sensitive: `M` (mega) is not the same as `m` (milli). If you want different aliases or case-insensitive parsing, add those aliases to the `Scales` you use. ## parse_or_clamp behavior The `parse_or_clamp` helper can be used when accepting user-provided suffixes that may be unknown or misspelled. Call it with `clamp = true` to interpret unknown suffixes as the largest available magnitude in the active `Scales`. Important: when a `Scales` provides an `explicit_map` (for example `Scales::Time()`), `parse_or_clamp` will clamp to the largest multiplier defined in that explicit map rather than assuming a power-of-base value. This ensures consistent behavior for non-power-of-base scales such as time units (where the largest defined unit is `Gyr`). ## Testing and correctness notes We added a number of focused tests to ensure robust handling of edge-cases: - Micro sign parsing and optional micro-sign output (tests/micro_sign.rs). - Forced-suffix behavior including unknown-suffix fallbacks (tests/forced_suffix.rs). - SI round-trip formatting for new prefixes `R`/`Q` and parsing validation (tests/si_roundtrip.rs). - Time scale edge cases: months, quarters, parsing case-sensitivity, and clamp behavior (tests/time_edgecases.rs). If you rely on case-insensitive parsing or custom aliases, construct a `Scales` with the aliases you need and pass it to `Formatter::with_scales`. ## Contributing Contributions are welcome. When you add features, please include tests. ## License See the repository `LICENSE` file for license details. human_format-1.2.1/_config.yml000064400000000000000000000003301046102023000144300ustar 00000000000000remote_theme: pages-themes/architect@v0.2.0 plugins: - jekyll-remote-theme title: human_format description: Rust Port of human-format from node, formatting numbers for us, while the machines are still at bay. human_format-1.2.1/changelog.md000064400000000000000000000072221046102023000145610ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [1.2.1] - 2026-01-16 ### Fixed - Binary suffix ki should be Kis - [PR#34](https://github.com/BobGneu/human-format-rs/pull/34) by [ip1981](https://github.com/ip1981) ## [1.2.0] - 2025-12-27 ### Added - New tests for cases out in the wild - `try_parse` which returns `Result` instead of panicking - `ParseError` enum with variants for `EmptyInput`, `InvalidNumber`, and `UnknownSuffix` - `parse_or_clamp` convenience method to optionally clamp unknown suffixes to the largest multiplier (now respects `explicit_map` when present) - Doctests and README snippets demonstrating `try_parse`, binary scales, units and negative numbers - Support for the newest SI prefixes: `R`/`Q` (ronna/quetta) and `r`/`q` (ronto/quecto) - Optional micro sign formatting and parsing: accept `µ` and output `µ` when enabled via `Formatter::with_micro_sign(true)` - Forced suffix formatting: `Formatter::with_suffix("M")` will scale output to the requested suffix when possible (e.g., `100000 -> 0.10 M`) - New `Scales::Time()` with explicit time unit multipliers (`ns`, `us`, `ms`, `s`, `m`, `h`, `d`, `w`, `mo`, `qtr`, `y`, `dec`, `c`, `kyr`, `Myr`, `Gyr`, and aliases) ### Changed - A few tweaks to our build flow to run clippy, and make sure to gate building based on prior dependent actions - removed verbose flag as it does not appear to be necessary any longer - Renamed `master` branch to `main` - lower casing si suffixes - [PR#27](https://github.com/BobGneu/human-format-rs/pull/27) by [jdrouet](https://github.com/jdrouet) - Replaced silent clamping/0.0 multiplier lookup with explicit `try_get_magnitude_multiplier` returning error on unknown suffix - Refactored parsing internals to centralize numeric/suffix extraction and reduce duplication - Added edge-case tests (empty input, trailing garbage, comma-decimal behavior, NaN/Infinity, rounding boundaries, and very large magnitudes) ### Removed - Removed `galvanic-test` as a dependency ## [1.1.0] - 2024-02-16 ### Changed - Format check included in build - Improve error handling in try_parse with better ergonomics - [PR#19](https://github.com/BobGneu/human-format-rs/pull/19) by [@jgrund](https://github.com/jgrund) ### Removed - removed Travis & Appveyor ## [1.0.3] - 2019-11-23 ### Fixed - Removed unnecessary logging - [PR#9](https://github.com/BobGneu/human-format-rs/pull/9) by [@jaysonsantos](https://github.com/jaysonsantos) - Corrected binary base to 1024 ## [1.0.2] - 2018-02-01 ### Fixed - Corrected issue with API, expecting owned strings when the common occurrence will be references. ## [1.0.1] - 2018-01-28 ### Added - Updated Documentation to improve utility of [docs.rs](https://docs.rs/crate/human_format/) - Added fmt to build scripts ## [1.0.0] - 2018-01-28 Initial Release [unreleased]: https://github.com/BobGneu/human-format-rs/compare/1.2.1...develop [1.2.1]: https://github.com/BobGneu/human-format-rs/compare/1.2.0...1.2.1 [1.2.0]: https://github.com/BobGneu/human-format-rs/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/BobGneu/human-format-rs/compare/1.0.3...1.1.0 [1.0.3]: https://github.com/BobGneu/human-format-rs/compare/1.0.2...1.0.3 [1.0.2]: https://github.com/BobGneu/human-format-rs/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/BobGneu/human-format-rs/compare/1.0.0...1.0.1 [1.0.0]: https://github.com/BobGneu/human-format-rs/tree/1.0.0 human_format-1.2.1/sonar-project.properties000064400000000000000000000006351046102023000172150ustar 00000000000000sonar.projectKey=BobGneu_human-format-rs sonar.organization=gneu-projects # This is the name and version displayed in the SonarCloud UI. sonar.projectName=human-format-rs sonar.projectVersion=1.1.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. sonar.sources=src/ # Encoding of the source code. Default is default system encoding sonar.sourceEncoding=UTF-8 human_format-1.2.1/src/lib.rs000064400000000000000000000466421046102023000142240ustar 00000000000000#![doc(html_root_url = "https://docs.rs/human_format")] //! `human_format` is a library for formatting numbers into human readable strings. It supports SI, Time and Binary scales out of the box, and allows for custom scales as well. //! //! ## Setup //! //! Add the library to your dependencies listing //! //! ```bash //! $ cargo add human_format //! ``` //! //! ## Usage //! //! Print human readable strings from numbers using SI scales by default //! //! ```rust //! // "1.00 k" //! let tmpStr = human_format::Formatter::new() //! .format(1000.0); //! # assert_eq!(tmpStr, "1.00 k"); //! //! // "1.00 M" //! let tmpStr2 = human_format::Formatter::new() //! .format(1000000.0); //! # assert_eq!(tmpStr2, "1.00 M"); //! //! // "1.00 G" //! let tmpStr3 = human_format::Formatter::new() //! .format(1000000000.0); //! # assert_eq!(tmpStr3, "1.00 G"); //! ``` //! //! If you are so inspired you can even try playing with units and customizing your `Scales` //! //! For more examples you should review the examples on github: [tests/demo.rs](https://github.com/BobGneu/human-format-rs/blob/master/tests/demo.rs) //! #[derive(Debug)] struct ScaledValue { value: f64, suffix: String, } /// Entry point to the library. Use this to handle your formatting needs. #[derive(Debug)] pub struct Formatter { decimals: usize, separator: String, scales: Scales, forced_units: String, forced_suffix: String, use_micro_sign: bool, } impl Default for Formatter { fn default() -> Self { Formatter { decimals: 2, separator: " ".to_owned(), scales: Scales::new(), forced_units: "".to_owned(), forced_suffix: "".to_owned(), use_micro_sign: false, } } } /// Provide a customized scaling scheme for your own modeling. #[derive(Debug)] pub struct Scales { base: u32, suffixes: Vec, suffixes_neg: Vec, explicit_map: Option>, } impl Formatter { /// Initializes a new `Formatter` with default values. pub fn new() -> Self { Default::default() } /// Sets the decimals value for formatting the string. pub fn with_decimals(&mut self, decimals: usize) -> &mut Self { self.decimals = decimals; self } /// Sets the separator value for formatting the string. pub fn with_separator(&mut self, separator: &str) -> &mut Self { self.separator = separator.to_owned(); self } /// Sets the scales value. pub fn with_scales(&mut self, scales: Scales) -> &mut Self { self.scales = scales; self } /// Sets the units value. pub fn with_units(&mut self, units: &str) -> &mut Self { self.forced_units = units.to_owned(); self } /// Sets the expected suffix value. pub fn with_suffix(&mut self, suffix: &str) -> &mut Self { self.forced_suffix = suffix.to_owned(); self } /// Enable using the micro sign `µ` in formatted output when fractional suffix is `u`. pub fn with_micro_sign(&mut self, enable: bool) -> &mut Self { self.use_micro_sign = enable; self } /// Formats the number into a string pub fn format(&self, value: f64) -> String { // Handle non-finite values explicitly to avoid loops in scaling logic if value.is_nan() { return "NaN".to_owned(); } if value < 0.0 { return format!("-{}", self.format(-value)); } if value.is_infinite() { return "inf".to_owned(); } // If a forced suffix is provided, attempt to scale to that suffix let scaled_value = if !self.forced_suffix.is_empty() { // normalize micro sign in forced suffix when looking up let lookup = self.forced_suffix.replace('\u{00B5}', "u"); match self.scales.try_get_magnitude_multiplier(&lookup) { Ok(mult) => ScaledValue { value: value / mult, suffix: self.forced_suffix.clone(), }, Err(_) => self.scales.to_scaled_value(value), } } else { self.scales.to_scaled_value(value) }; let out_suffix = if self.use_micro_sign && scaled_value.suffix == "u" { "µ".to_owned() } else { scaled_value.suffix.clone() }; format!( "{:.width$}{}{}{}", scaled_value.value, self.separator, out_suffix, self.forced_units, width = self.decimals ) } /// Parse a string back into a float value. /// /// This convenience wrapper unwraps the result of `try_parse` and will panic /// on malformed input. It is feature-gated behind `panic_parse` so that /// consumers must opt-in to the panicking API via the panic_parse feature. #[cfg(feature = "panic_parse")] #[deprecated( note = "Use `try_parse`, which returns `Result` and does not panic on malformed input" )] pub fn parse(&self, value: &str) -> f64 { self.try_parse(value).unwrap() } /// Attempt to parse a string back into a float value. /// /// Examples: /// /// ```rust /// use human_format::{Formatter, Scales}; /// // SI example /// let f = Formatter::new(); /// assert_eq!(f.try_parse("1.00 k").unwrap(), 1000.0); /// // Binary scales (Ki = 1024) /// let mut fbin = Formatter::new(); /// fbin.with_scales(Scales::Binary()); /// assert_eq!(fbin.try_parse("1.00 Ki").unwrap(), 1024.0); /// // Units specified via with_units() are automatically stripped from input /// let mut funit = Formatter::new(); /// funit.with_units("B"); /// assert_eq!(funit.try_parse("1.00 kB").unwrap(), 1000.0); /// // Negative numbers /// assert_eq!(Formatter::new().try_parse("-1.0 k").unwrap(), -1000.0); /// // Invalid input /// assert!(Formatter::new().try_parse("bad input").is_err()); /// ``` pub fn try_parse(&self, value: &str) -> Result { let (number_str, suffix) = self.parse_components(value)?; let number = number_str .parse::() .map_err(ParseError::InvalidNumber)?; let magnitude_multiplier = self.scales.try_get_magnitude_multiplier(&suffix)?; Ok(number * magnitude_multiplier) } fn parse_components(&self, value: &str) -> Result<(String, String), ParseError> { // Remove forced units if present let value = value .trim() .trim_end_matches(&self.forced_units) .to_string(); // Extract leading number (allow sign and decimal) let mut number = String::new(); for (i, c) in value.chars().enumerate() { if c.is_ascii_digit() || c == '.' || (c == '-' && i == 0) { number.push(c); } else { break; } } if number.is_empty() { return Err(ParseError::EmptyInput); } let suffix = value .trim_start_matches(&number) .trim_start_matches(&self.separator) .to_string(); // Normalize common variants: acceptance of micro sign 'µ' let suffix = suffix.replace('\u{00B5}', "u"); Ok((number, suffix)) } /// Parse a string and optionally clamp unknown suffixes to the largest suffix multiplier. /// /// If `clamp` is `false`, this behaves like `try_parse` and returns an error on unknown suffixes. /// If `clamp` is `true`, unknown suffixes will be interpreted as the largest available suffix. /// /// Examples: /// /// ```rust /// use human_format::{Formatter, Scales}; /// let f = Formatter::new(); /// // Unknown suffix errors when clamp == false /// assert!(f.parse_or_clamp("1.0 DN", false).is_err()); /// // Unknown suffix clamps to largest suffix multiplier when clamp == true /// assert!(f.parse_or_clamp("1.0 DN", true).is_ok()); /// // Binary example with units /// let mut fb = Formatter::new(); /// fb.with_scales(Scales::Binary()).with_units("B"); /// assert_eq!(fb.parse_or_clamp("1.0 KiB", false).unwrap(), 1024.0); /// // Negative number with clamp /// assert_eq!(Formatter::new().parse_or_clamp("-1.0 k", true).unwrap(), -1000.0); /// ``` pub fn parse_or_clamp(&self, value: &str, clamp: bool) -> Result { let (number_str, suffix) = self.parse_components(value)?; let number = number_str .parse::() .map_err(ParseError::InvalidNumber)?; match self.scales.try_get_magnitude_multiplier(&suffix) { Ok(mult) => Ok(number * mult), Err(ParseError::UnknownSuffix(_)) if clamp => { // If scales has an explicit_map (e.g., Time), clamp to the // largest explicit multiplier rather than assuming a power of // `base` matching the last suffix index. if let Some(map) = &self.scales.explicit_map && !map.is_empty() { let max_mult = map.values().copied().fold(f64::NEG_INFINITY, f64::max); return Ok(number * max_mult); } let last_index = self.scales.suffixes.len().saturating_sub(1); let mult = (self.scales.base as f64).powi(last_index as i32); Ok(number * mult) } Err(e) => Err(e), } } } /// Errors returned by parsing operations. #[derive(Debug)] pub enum ParseError { EmptyInput, InvalidNumber(std::num::ParseFloatError), UnknownSuffix(String), } impl PartialEq for ParseError { fn eq(&self, other: &Self) -> bool { match (self, other) { (ParseError::EmptyInput, ParseError::EmptyInput) => true, // Note: `ParseFloatError` does not implement `PartialEq` on stable Rust. // We therefore treat all `InvalidNumber(_)` variants as equal to each other, // ignoring the inner `ParseFloatError`. (ParseError::InvalidNumber(_), ParseError::InvalidNumber(_)) => true, (ParseError::UnknownSuffix(a), ParseError::UnknownSuffix(b)) => a == b, _ => false, } } } impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ParseError::EmptyInput => write!(f, "Empty input"), ParseError::InvalidNumber(e) => write!(f, "Invalid number: {}", e), ParseError::UnknownSuffix(s) => write!(f, "Unknown suffix: {}", s), } } } impl std::error::Error for ParseError {} impl Default for Scales { fn default() -> Self { Scales::SI() } } impl Scales { /// Instantiates a new `Scales` with SI keys pub fn new() -> Self { Default::default() } /// Instantiates a new `Scales` with SI keys #[allow(non_snake_case)] pub fn SI() -> Self { Scales { base: 1000, suffixes: vec![ "".to_owned(), "k".to_owned(), "M".to_owned(), "G".to_owned(), "T".to_owned(), "P".to_owned(), "E".to_owned(), "Z".to_owned(), "Y".to_owned(), "R".to_owned(), "Q".to_owned(), ], suffixes_neg: vec![ "".to_owned(), "m".to_owned(), // milli "u".to_owned(), // micro (use 'u' ascii for micro) "n".to_owned(), // nano "p".to_owned(), // pico "f".to_owned(), // femto "a".to_owned(), // atto "z".to_owned(), // zepto "y".to_owned(), // yocto "r".to_owned(), // ronto (10^-27) "q".to_owned(), // quecto (10^-30) ], explicit_map: None, } } /// Instantiates a new `Scales` with Binary keys #[allow(non_snake_case)] pub fn Binary() -> Self { Scales { base: 1024, suffixes: vec![ "".to_owned(), "Ki".to_owned(), "Mi".to_owned(), "Gi".to_owned(), "Ti".to_owned(), "Pi".to_owned(), "Ei".to_owned(), "Zi".to_owned(), "Yi".to_owned(), "Ri".to_owned(), "Qi".to_owned(), ], // binary scales usually don't define fractional SI-like prefixes; keep empty placeholder suffixes_neg: vec!["".to_owned()], explicit_map: None, } } /// Instantiates a new `Scales` for time units. /// /// This maps common time suffixes to multipliers in seconds. #[allow(non_snake_case)] pub fn Time() -> Self { use std::collections::HashMap; let mut map: HashMap = HashMap::new(); map.insert("ns".to_owned(), 1e-9); map.insert("us".to_owned(), 1e-6); map.insert("ms".to_owned(), 1e-3); map.insert("s".to_owned(), 1.0); map.insert("m".to_owned(), 60.0); map.insert("h".to_owned(), 3600.0); map.insert("d".to_owned(), 86400.0); map.insert("w".to_owned(), 604800.0); // longer period units using average definitions let year_secs = 365.2425 * 86400.0; // average Gregorian year let month_secs = year_secs / 12.0; // average month map.insert("mo".to_owned(), month_secs); map.insert("month".to_owned(), month_secs); // quarters: three-month periods map.insert("qtr".to_owned(), 3.0 * month_secs); map.insert("y".to_owned(), year_secs); map.insert("yr".to_owned(), year_secs); map.insert("year".to_owned(), year_secs); map.insert("dec".to_owned(), 10.0 * year_secs); map.insert("decade".to_owned(), 10.0 * year_secs); map.insert("c".to_owned(), 100.0 * year_secs); map.insert("century".to_owned(), 100.0 * year_secs); map.insert("kyr".to_owned(), 1000.0 * year_secs); // millennium (kilo-year) map.insert("millennium".to_owned(), 1000.0 * year_secs); map.insert("Myr".to_owned(), 1.0e6 * year_secs); map.insert("Gyr".to_owned(), 1.0e9 * year_secs); Scales { base: 1, suffixes: vec![], suffixes_neg: vec![], explicit_map: Some(map), } } /// Sets the base for the `Scales` pub fn with_base(&mut self, base: u32) -> &mut Self { self.base = base; self } /// Sets the suffixes listing appropriately pub fn with_suffixes(&mut self, suffixes: Vec<&str>) -> &mut Self { self.suffixes = Vec::new(); for suffix in suffixes { // This should be to_owned to be clear about intent. // https://users.rust-lang.org/t/to-string-vs-to-owned-for-string-literals/1441/6 self.suffixes.push(suffix.to_owned()); } self } fn try_get_magnitude_multiplier(&self, value: &str) -> Result { // If an explicit mapping exists (e.g., time units), prefer it if let Some(map) = &self.explicit_map && let Some(val) = map.get(value) { return Ok(*val); } // positive suffixes if let Some((idx, _)) = self.suffixes.iter().enumerate().find(|(_, x)| x == &value) { return Ok((self.base as f64).powi(idx as i32)); } // negative suffixes (fractions) if let Some((idx, _)) = self .suffixes_neg .iter() .enumerate() .find(|(_, x)| x == &value) { // idx 0 corresponds to base^0 (no scaling); idx 1 => base^-1, idx 2 => base^-2 let exp = -(idx as i32); return Ok((self.base as f64).powi(exp)); } // build valid suffix list for error message let mut valid: Vec = Vec::new(); if let Some(map) = &self.explicit_map { valid.extend(map.keys().cloned()); } valid.extend( self.suffixes .iter() .filter(|x| !x.trim().is_empty()) .cloned(), ); valid.extend( self.suffixes_neg .iter() .filter(|x| !x.trim().is_empty()) .cloned(), ); Err(ParseError::UnknownSuffix(format!( "{}; valid suffixes are: {}", value, valid.join(", ") ))) } fn to_scaled_value(&self, value: f64) -> ScaledValue { let mut index: usize = 0; let base: f64 = self.base as f64; let mut value = value; // Prevent infinite loops for non-finite values and cap index to available suffixes let last_index = self.suffixes.len().saturating_sub(1); let last_neg = self.suffixes_neg.len().saturating_sub(1); // If explicit map provided (e.g., Time), prefer selecting suffix from it if let Some(map) = &self.explicit_map { // Build vector of (suffix, multiplier) and sort descending by multiplier let mut entries: Vec<(String, f64)> = map.iter().map(|(k, v)| (k.clone(), *v)).collect(); entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); if value > 0.0 { for (suf, mult) in entries.iter() { if value >= *mult { return ScaledValue { value: value / *mult, suffix: suf.clone(), }; } } // If smaller than smallest multiplier, use smallest (e.g., ns) if let Some((suf, mult)) = entries.last() { return ScaledValue { value: value / *mult, suffix: suf.clone(), }; } } } if value >= base { while value >= base && index < last_index { value /= base; index += 1; } ScaledValue { value, suffix: self.suffixes[index].to_owned(), } } else if value > 0.0 && value < 1.0 { // Use negative prefixes for fractional values let mut neg_idx: usize = 0; while value < 1.0 && neg_idx < last_neg { value *= base; neg_idx += 1; } ScaledValue { value, suffix: self.suffixes_neg[neg_idx].to_owned(), } } else { ScaledValue { value, suffix: self.suffixes[0].to_owned(), } } } } human_format-1.2.1/tests/demo.rs000064400000000000000000000176361046102023000147560ustar 00000000000000extern crate human_format; #[cfg(test)] mod demo_examples { use human_format::*; #[test] fn should_allow_explicit_decimals() { assert_eq!( Formatter::new().with_decimals(1).format(1000 as f64), "1.0 k" ); } #[test] fn should_allow_explicit_separator() { assert_eq!( Formatter::new().with_separator(" - ").format(1000 as f64), "1.00 - k" ); } #[test] fn should_allow_use_of_si_scale_explicitly() { assert_eq!( Formatter::new() .with_scales(Scales::SI()) .format(1000 as f64), "1.00 k" ); } #[test] fn should_allow_use_of_binary_scale_explicitly() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .format(1024 as f64), "1.00 Ki" ); } #[test] fn should_allow_use_of_binary_units_explicitly() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .format(102400 as f64), "100.00 KiB" ); } #[test] fn should_output_10_24_mib() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .format(1024.0 * 1024.0 as f64), "1.00 MiB" ); } #[test] fn should_output_75_11_pib() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .format(84_567_942_345_572_238.0), "75.11 PiB" ); } #[test] fn should_output_1_00_gbps() { assert_eq!(Formatter::new().with_units("B/s").format(1e9), "1.00 GB/s"); } #[test] fn should_allow_explicit_suffix_and_unit() { assert_eq!( Formatter::new() .with_suffix("k") .with_units("m") .format(1024 as f64), "1.02 km" ); } #[test] fn should_allow_use_of_explicit_scale() { let mut scales = Scales::new(); scales .with_base(1024) .with_suffixes(vec!["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"]); assert_eq!( Formatter::new() .with_scales(scales) .with_units("B") .format(1024 as f64), "1.00 KiB" ); } #[test] fn should_allow_parsing_to_f64() { assert_eq!(Formatter::new().try_parse("1.00 k").unwrap(), 1000.0); } #[test] fn should_allow_try_parsing_to_f64() { assert_eq!(Formatter::new().try_parse("1.00 M"), Ok(1000000.0)); } #[test] fn should_allow_parsing_binary_values_to_f64() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .try_parse("1.00 Ki") .unwrap(), 1024.0 ); } #[test] fn should_allow_parsing_binary_values_with_units_to_f64() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .try_parse("1.00 KiB") .unwrap(), 1024.0 ); } #[test] fn should_allow_try_parsing_binary_values_with_units_to_f64() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .try_parse("1.00 KiB"), Ok(1024.0) ); } #[test] fn should_surface_errors() { let result = Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .try_parse("1.00 DN"); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.to_string().contains("Unknown suffix")); } #[test] fn try_parse_explicit_suffix_and_unit() { assert_eq!( Formatter::new().with_units("m").try_parse("1.024Mm"), Ok(1024000.0) ); } #[test] fn try_parse_explicit_suffix_and_unitless() { assert_eq!( Formatter::new().with_units("m").try_parse("1.024M"), Ok(1024000.0) ); } #[test] fn try_parse_very_large_value() { assert_eq!( Formatter::new().with_units("B").try_parse("2PB"), Ok(2_000_000_000_000_000.0) ); } #[test] fn formatting_uses_micro_sign_when_enabled() { let mut f = Formatter::new(); f.with_micro_sign(true); // 0.000001 -> 1.00 µ assert_eq!(f.format(0.000001_f64), "1.00 µ"); } #[test] fn time_scale_format_and_parse() { // 90 seconds should be formatted to 1.50 m when using time scales (minutes) let mut f = Formatter::new(); f.with_scales(Scales::Time()); assert_eq!(f.format(90.0), "1.50 m"); // Parsing "1.5 m" with time scales should give 90 seconds let mut fp = Formatter::new(); fp.with_scales(Scales::Time()).with_units("s"); assert_eq!(fp.try_parse("1.5 m").unwrap(), 90.0); } #[test] fn time_scale_long_units() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); // 1 month ~ average month in seconds let month_secs = (365.2425 * 86400.0) / 12.0; assert_eq!(f.try_parse("1 mo").unwrap(), month_secs); // 1 year let year_secs = 365.2425 * 86400.0; assert_eq!(f.try_parse("1 y").unwrap(), year_secs); // 1 decade = 10 years assert_eq!(f.try_parse("1 dec").unwrap(), 10.0 * year_secs); // 1 century = 100 years assert_eq!(f.try_parse("1 c").unwrap(), 100.0 * year_secs); } #[test] fn time_scale_quarters() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); let month_secs = (365.2425 * 86400.0) / 12.0; let quarter_secs = 3.0 * month_secs; assert_eq!(f.try_parse("1 qtr").unwrap(), quarter_secs); } #[test] fn forced_suffix_scaling() { let mut f = Formatter::new(); f.with_suffix("M"); // 100000 -> 0.10M (100000 / 1_000_000 = 0.1) assert_eq!(f.format(100000.0), "0.10 M"); } #[test] fn micro_sign_parsing_and_formatting() { // parsing accepts µ assert_eq!(Formatter::new().try_parse("1.0 µ").unwrap(), 1e-6); // formatting uses u by default let f = Formatter::new(); assert!(f.format(1e-6).contains("u") || f.format(1e-6).contains("µ")); // enable µ output let mut fm = Formatter::new(); fm.with_micro_sign(true); assert_eq!(fm.format(1e-6), "1.00 µ"); } #[test] fn forced_suffix_unknown_and_extremes() { // unknown forced suffix falls back let mut f = Formatter::new(); let s1 = f.with_suffix("DN").format(1000.0); assert_eq!(s1, "1.00 k"); // forcing very large suffix produces < 1 values let mut f2 = Formatter::new(); let s2 = f2.with_suffix("Q").format(1e3); assert_eq!(s2, "0.00 Q"); } #[test] fn time_scale_round_trip_and_case() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); // months and years let month_secs = (365.2425 * 86400.0) / 12.0; assert_eq!(f.try_parse("1 mo").unwrap(), month_secs); assert_eq!(f.try_parse("1 y").unwrap(), 365.2425 * 86400.0); // case sensitivity: use 'qtr' quarter alias assert_eq!(f.try_parse("1 qtr").unwrap(), 3.0 * month_secs); } } human_format-1.2.1/tests/edge/clamp_behavior.rs000064400000000000000000000014661046102023000177030ustar 00000000000000use human_format::*; #[test] fn parse_or_clamp_true_clamps_unknown_suffix() { let f = Formatter::new(); let res = f.parse_or_clamp("1.0 DN", true).unwrap(); assert!(res.is_finite()); } #[test] fn parse_or_clamp_clamps_to_largest_explicit_when_present() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); // unknown suffix with clamp=true should clamp to the largest explicit multiplier (Gyr) let parsed = f.parse_or_clamp("1.0 unknown_suffix", true).unwrap(); let year_secs = 365.2425 * 86400.0; let gyr = 1.0e9 * year_secs; assert_eq!(parsed, 1.0 * gyr); } #[test] fn parse_or_clamp_false_errors_on_unknown_suffix() { let f = Formatter::new(); let res = f.parse_or_clamp("1.0 DN", false); assert!(res.is_err()); } human_format-1.2.1/tests/edge/large_values.rs000064400000000000000000000003001046102023000173630ustar 00000000000000use human_format::*; #[test] fn very_large_magnitude_clamps_or_errors() { let f = Formatter::new(); let formatted = f.format(1e300); assert!(!formatted.is_empty()); } human_format-1.2.1/tests/edge/mod.rs000064400000000000000000000003701046102023000155000ustar 00000000000000// Consolidated edge-case tests mod clamp_behavior; mod large_values; mod nan_inf; mod parse_errors; mod rounding; pub use clamp_behavior::*; pub use large_values::*; pub use nan_inf::*; pub use parse_errors::*; pub use rounding::*; human_format-1.2.1/tests/edge/nan_inf.rs000064400000000000000000000004111046102023000163250ustar 00000000000000use human_format::*; #[test] fn nan_and_infinity_formatting() { assert_eq!(Formatter::new().format(0.0 / 0.0), "NaN"); assert_eq!(Formatter::new().format(f64::INFINITY), "inf"); assert_eq!(Formatter::new().format(f64::NEG_INFINITY), "-inf"); } human_format-1.2.1/tests/edge/parse_errors.rs000064400000000000000000000020201046102023000174210ustar 00000000000000use human_format::*; #[test] fn empty_input_errors() { let res = Formatter::new().try_parse(""); assert!(res.is_err()); assert_eq!(res.unwrap_err(), ParseError::EmptyInput); } #[test] fn whitespace_only_errors() { let res = Formatter::new().try_parse(" "); assert!(res.is_err()); assert_eq!(res.unwrap_err(), ParseError::EmptyInput); } #[test] fn trailing_garbage_errors() { let res = Formatter::new().try_parse("1.00 kxyz"); assert!(res.is_err()); let e = res.unwrap_err(); assert!(e.to_string().contains("Unknown suffix") || matches!(e, ParseError::UnknownSuffix(_))); } #[test] fn comma_decimal_behavior() { // Decide expected behavior: current implementation uses '.' only, so comma should error let res = Formatter::new().try_parse("1,23 k"); assert!(res.is_err()); } #[test] fn parse_negative_numbers() { let res = Formatter::new().try_parse("-1.0 k"); assert!(res.is_ok()); assert_eq!(res.unwrap(), -1000.0); } human_format-1.2.1/tests/edge/rounding.rs000064400000000000000000000003611046102023000165460ustar 00000000000000use human_format::*; #[test] fn rounding_boundaries() { let mut f = Formatter::new(); f.with_decimals(2); let formatted = f.format(999.995); assert!(formatted == "1000.00 " || formatted.starts_with("1.00 ")); } human_format-1.2.1/tests/forced_suffix.rs000064400000000000000000000021241046102023000166420ustar 00000000000000use human_format::{Formatter, Scales}; #[test] fn forced_suffix_unknown_falls_back_to_auto() { let mut f = Formatter::new(); f.with_suffix("nonexist"); // Forcing an unknown suffix should fall back to automatic scaling let s = f.format(1000.0); // automatic SI scaling for 1000 is "k" assert!((s.contains("k") || s.contains("K")) && !s.contains(" ki")); } #[test] fn forced_suffix_applies_multiplier_when_known() { let mut f = Formatter::new(); f.with_scales(Scales::SI()); f.with_suffix("k"); // forcing 'k' should divide value by 1000 let s = f.format(2000.0); assert!(s.contains("2.00") && s.contains("k")); } #[test] fn forced_suffix_respects_units_and_micro_sign_normalization() { let mut f = Formatter::new(); f.with_units("B"); // set forced suffix to the micro-sign variant; format should accept it f.with_suffix("µ"); let s = f.format(1.0e-6); // since suffix was forced to micro, output should include micro sign when enabled assert!(s.contains("µ") || s.contains("u")); } human_format-1.2.1/tests/formatting/binary.rs000064400000000000000000000011011046102023000174440ustar 00000000000000use human_format::*; #[test] fn binary_and_units_examples() { assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .format(1024.0), "1.00 ki" ); assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .format(102400.0), "100.00 KiB" ); assert_eq!( Formatter::new() .with_scales(Scales::Binary()) .with_units("B") .format(1024.0 * 1024.0), "1.00 MiB" ); } human_format-1.2.1/tests/formatting/forced.rs000064400000000000000000000007511046102023000174340ustar 00000000000000use human_format::*; #[test] fn forced_suffix_scaling_examples() { let mut f = Formatter::new(); f.with_suffix("M"); assert_eq!(f.format(100000.0), "0.10 M"); } #[test] fn forced_suffix_unknown_and_extremes_examples() { let mut f = Formatter::new(); let mut f2 = Formatter::new(); f.with_suffix("DN"); f2.with_suffix("Q"); let s = f.format(1000.0); assert!(!s.is_empty()); assert!(f2.format(1e3).starts_with("0.")); } human_format-1.2.1/tests/formatting/micro.rs000064400000000000000000000003031046102023000172740ustar 00000000000000use human_format::*; #[test] fn micro_sign_formatting_example() { let mut f = Formatter::new(); f.with_micro_sign(true); assert_eq!(f.format(0.000001_f64), "1.00 µ"); } human_format-1.2.1/tests/formatting/mod.rs000064400000000000000000000002311046102023000167420ustar 00000000000000mod binary; mod forced; mod micro; mod si; mod time; pub use binary::*; pub use forced::*; pub use micro::*; pub use si::*; pub use time::*; human_format-1.2.1/tests/formatting/si.rs000064400000000000000000000016671046102023000166140ustar 00000000000000use human_format::*; #[test] fn should_allow_use_of_si_scale_implicitly() { assert_eq!( Formatter::new().with_suffix("k").format(1000 as f64), "1.00 k" ); } #[test] fn should_handle_negative_values() { assert_eq!( Formatter::new().with_suffix("k").format(-1000 as f64), "-1.00 k" ); } #[test] fn should_handle_large_numbers_in_scientific_notation() { assert_eq!(Formatter::new().format(1.2123123422324232e16), "12.12 P"); assert_eq!(Formatter::new().format(1.2123123422324232e18), "1.21 E"); assert_eq!(Formatter::new().format(1.2123123422324232e26), "121.23 Y"); assert_eq!(Formatter::new().format(5.58559632792669e27), "5.59 R"); assert_eq!(Formatter::new().format(5.58559632792669e30), "5.59 Q"); assert_eq!(Formatter::new().format(5.58559632792669e31), "55.86 Q"); assert_eq!(Formatter::new().format(5.58559632792669e35), "558559.63 Q"); } human_format-1.2.1/tests/formatting/time.rs000064400000000000000000000002661046102023000171310ustar 00000000000000use human_format::*; #[test] fn time_format_example() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); assert_eq!(f.format(90.0), "1.50 m"); } human_format-1.2.1/tests/micro_sign.rs000064400000000000000000000014001046102023000161410ustar 00000000000000use human_format::Formatter; #[test] fn parse_accepts_micro_sign_and_ascii_u() { let f = Formatter::new(); // ASCII 'u' should parse as micro assert_eq!(f.try_parse("1.0 u").unwrap(), 1.0e-6); // Unicode micro sign 'µ' should also parse the same assert_eq!(f.try_parse("1.0 µ").unwrap(), 1.0e-6); } #[test] fn format_has_optional_micro_sign_output() { let mut f = Formatter::new(); f.with_suffix("u"); // Default formatting uses ascii 'u' for micro let s = f.format(1.0e-6); assert!(s.contains("u")); // Enable micro sign output let mut f2 = Formatter::new(); f2.with_suffix("u"); f2.with_micro_sign(true); let s2 = f2.format(1.0e-6); assert!(s2.contains("µ")); } human_format-1.2.1/tests/parse_clamp.rs000064400000000000000000000011131046102023000162770ustar 00000000000000use human_format::{Formatter, Scales}; #[test] fn explicit_map_clamps_to_largest_multiplier() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); // Choose a suffix that is unknown; with clamp=true this should clamp // to the largest explicit multiplier (Gyr -> 1e9 * year_secs) let parsed = f.parse_or_clamp("1.0 unknown_suffix", true).unwrap(); // The largest multiplier in Time() explicit map is Gyr (1e9 * year_secs) let year_secs = 365.2425 * 86400.0; let gyr = 1.0e9 * year_secs; assert_eq!(parsed, 1.0 * gyr); } human_format-1.2.1/tests/parsing/clamp.rs000064400000000000000000000005101046102023000165500ustar 00000000000000use human_format::*; #[test] fn parse_or_clamp_largest_suffix() { let f = Formatter::new(); // unknown suffix errors when clamp == false assert!(f.parse_or_clamp("1.0 DN", false).is_err()); // clamp to largest suffix (Q -> 10^30) assert_eq!(f.parse_or_clamp("1.0 DN", true).unwrap(), 1e30); } human_format-1.2.1/tests/parsing/micro.rs000064400000000000000000000003401046102023000165660ustar 00000000000000use human_format::*; #[test] fn micro_sign_input_is_accepted() { let f = Formatter::new(); // micro sign input should be accepted and treated as 'u' assert_eq!(f.try_parse("1.0 µ").unwrap(), 1e-6); } human_format-1.2.1/tests/parsing/mod.rs000064400000000000000000000001761046102023000162430ustar 00000000000000mod clamp; mod micro; mod si; mod suffixes; pub use clamp::*; pub use micro::*; pub use si::*; pub use suffixes::*; human_format-1.2.1/tests/parsing/si.rs000064400000000000000000000026301046102023000160740ustar 00000000000000use human_format::*; #[test] fn should_parse_1_0_g_as_1000000000() { let formatter = Formatter::new(); assert_eq!(formatter.try_parse("1.0 G").unwrap(), 10.0_f64.powf(9.0)); } #[test] fn should_parse_11248924551_k_as_1_1248924551e13() { let formatter = Formatter::new(); assert_eq!( formatter.try_parse("11248924551 k").unwrap(), 1.1248924551e13 ); } #[test] fn should_parse_55_86_q_as_5_586_e31() { let formatter = Formatter::new(); assert_eq!(formatter.try_parse("55.86 Q").unwrap(), 5.586e31); } #[test] fn should_parse_558559_63_q_as_5_5855963e35() { let formatter = Formatter::new(); assert_eq!(formatter.try_parse("558559.63 Q").unwrap(), 5.5855963e35); } #[test] fn should_parse_1494_k_as_1494222() { let mut formatter = Formatter::new(); formatter.with_decimals(3); assert_eq!(formatter.try_parse("1494 k").unwrap(), 1494000.0); } #[test] fn round_trip_ronna_and_quetta() { let f = Formatter::new(); // format and parse back large SI values let s = f.format(1e27); assert_eq!(f.try_parse(&s).unwrap(), 1e27); let s2 = f.format(1e30); assert_eq!(f.try_parse(&s2).unwrap(), 1e30); // and small-side prefixes let s3 = f.format(1e-27); assert_eq!(f.try_parse(&s3).unwrap(), 1e-27); let s4 = f.format(1e-30); assert_eq!(f.try_parse(&s4).unwrap(), 1e-30); } human_format-1.2.1/tests/parsing/suffixes.rs000064400000000000000000000010441046102023000173130ustar 00000000000000use human_format::*; #[test] fn should_format_and_parse_ronna_and_quetta() { let f = Formatter::new(); // 1 R -> 10^27 assert_eq!(f.try_parse("1.0 R").unwrap(), 1.0e27); // 55.86 Q -> 55.86 * 10^30 assert_eq!(f.try_parse("55.86 Q").unwrap(), 55.86e30); } #[test] fn should_parse_ronto_and_quecto_and_micro_sign() { let f = Formatter::new(); // 1 m -> milli assert_eq!(f.try_parse("1.0 m").unwrap(), 1e-3); // 1 r -> ronto (10^-27) assert_eq!(f.try_parse("1.0 r").unwrap(), 1e-27); } human_format-1.2.1/tests/si_roundtrip.rs000064400000000000000000000012161046102023000165360ustar 00000000000000use human_format::{Formatter, Scales}; #[test] fn si_round_trip_new_prefixes() { let mut f = Formatter::new(); f.with_scales(Scales::SI()); // Test quetta (Q) and ronna (R) let q = f.format(1.0e30); assert!(q.contains("Q") || q.contains("quetta")); let r = f.format(1.0e27); assert!(r.contains("R") || r.contains("ronna")); // Round-trip via parse for these outputs should return approx original let parsed_q = f.try_parse(&q).unwrap(); let parsed_r = f.try_parse(&r).unwrap(); assert!((parsed_q / 1.0e30 - 1.0).abs() < 1e-12); assert!((parsed_r / 1.0e27 - 1.0).abs() < 1e-12); } human_format-1.2.1/tests/time_edgecases.rs000064400000000000000000000037101046102023000167570ustar 00000000000000use human_format::{Formatter, Scales}; #[test] fn time_month_and_year_parsing_and_formatting() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); // 1 month should be roughly year_secs/12 seconds let year_secs = 365.2425 * 86400.0; let month_secs = year_secs / 12.0; // Format 1 month in seconds should yield about "1.00 mo" when forced f.with_suffix("mo"); let s = f.format(month_secs); assert!(s.contains("1.00") && s.contains("mo")); // Parsing month string should return month_secs let parsed = f.try_parse("1.0 mo").unwrap(); assert!((parsed / month_secs - 1.0).abs() < 1e-12); } #[test] fn quarters_are_three_months_and_parse() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); // Quarter is defined as three months in the Time() mapping let year_secs = 365.2425 * 86400.0; let qtr_secs = 3.0 * (year_secs / 12.0); // Forced suffix qtr should yield 1.00 when formatting qtr_secs f.with_suffix("qtr"); let s = f.format(qtr_secs); assert!(s.contains("1.00") && s.contains("qtr")); // Parsing "1 qtr" should return qtr_secs let parsed = f.try_parse("1 qtr").unwrap(); assert!((parsed / qtr_secs - 1.0).abs() < 1e-12); } #[test] fn time_parsing_is_case_sensitive_for_explicit_map() { // with Time scales 'mo' and 'month' are lower-case; 'Mo' should be unknown let mut ft = Formatter::new(); ft.with_scales(Scales::Time()); assert!(ft.try_parse("1 mo").is_ok()); assert!(ft.try_parse("1 Mo").is_err()); } #[test] fn parse_or_clamp_uses_explicit_map_largest_for_time() { let mut f = Formatter::new(); f.with_scales(Scales::Time()); // Unknown suffix clamps to Gyr multiplier when clamp=true let parsed = f.parse_or_clamp("2.0 unknown", true).unwrap(); let year_secs = 365.2425 * 86400.0; let gyr = 1.0e9 * year_secs; assert_eq!(parsed, 2.0 * gyr); }