Skip to content

Percentage Calculator — Rust source

Calculate percentages three ways - X% of Y, X is what percent of Y, and the percentage change between two values. Runs entirely in your browser, with a shareable link.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! # percentage-calculator — Rust port.
//!
//! CosmoDev polyglot showcase port of the `percentage-calculator` tool. Pure
//! percentage logic ported from `src/lib/percentage.ts`. Deterministic and
//! side-effect free: every function returns `None` for non-finite input or an
//! undefined result (a zero divisor) instead of panicking, and rounds to a
//! configurable maximum number of decimal places.
//!
//! Display source — part of CosmoDev's polyglot tool pages.

/// Options governing result precision. `max_decimals` defaults to 2 when
/// `None`, mirroring the TypeScript `PercentOptions` optional field — which
/// (unlike a plain `u32`) distinguishes "unset" from an explicit `0`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PercentOptions {
    pub max_decimals: Option<u32>,
}

const DEFAULT_MAX_DECIMALS: u32 = 2;

/// Reports whether every value in `vals` is finite. `NaN` and ±infinity are
/// treated as invalid inputs throughout this module: callers receive `None`
/// rather than a propagated `NaN`.
fn every_finite(vals: &[f64]) -> bool {
    vals.iter().all(|v| v.is_finite())
}

/// Resolve the effective precision, falling back to the default when unset.
fn resolve_decimals(opts: PercentOptions) -> u32 {
    opts.max_decimals.unwrap_or(DEFAULT_MAX_DECIMALS)
}

/// Round `n` to at most `max_decimals` places.
///
/// Adding `f64::EPSILON` before scaling absorbs the tiny errors that arise
/// from representing decimal fractions in binary floating point (the classic
/// `0.1 + 0.2 != 0.3`). Non-finite values are returned unchanged so this
/// function is total.
///
/// Note: the TypeScript source gives `max_decimals` a default of 2. Rust has
/// no default parameters, so callers pass it explicitly — the three `percent_*`
/// functions handle the default for you via [`PercentOptions`].
pub fn round(n: f64, max_decimals: u32) -> f64 {
    if !n.is_finite() {
        return n;
    }
    let factor = 10_f64.powi(max_decimals as i32);
    ((n + f64::EPSILON) * factor).round() / factor
}

/// X% of `value`: `(pct / 100.0) * value`. Returns `None` when either input is
/// non-finite.
pub fn percent_of(pct: f64, value: f64, opts: PercentOptions) -> Option<f64> {
    if !every_finite(&[pct, value]) {
        return None;
    }
    Some(round((pct / 100.0) * value, resolve_decimals(opts)))
}

/// What percentage `part` is of `total`: `(part / total) * 100.0`. Returns
/// `None` when `total` is zero (the ratio is undefined) or either input is
/// non-finite.
pub fn what_percent(part: f64, total: f64, opts: PercentOptions) -> Option<f64> {
    if !every_finite(&[part, total]) {
        return None;
    }
    if total == 0.0 {
        return None;
    }
    Some(round((part / total) * 100.0, resolve_decimals(opts)))
}

/// Percentage change from `from` to `to`: `((to - from) / from.abs()) * 100.0`.
///
/// The denominator is absolute so the result's sign reflects only the
/// direction of change (positive for an increase, negative for a decrease).
/// Returns `None` when `from` is zero (no meaningful base to compare against)
/// or either input is non-finite.
pub fn percent_change(from: f64, to: f64, opts: PercentOptions) -> Option<f64> {
    if !every_finite(&[from, to]) {
        return None;
    }
    if from == 0.0 {
        return None;
    }
    Some(round(((to - from) / from.abs()) * 100.0, resolve_decimals(opts)))
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →