Percentage Calculator — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// percentage-calculator — Zig port: three-way percentage math (X% of Y, X as % of Y, % change).
const std = @import("std");
/// Rounding precision when an option is omitted, matching
/// DEFAULT_MAX_DECIMALS in src/lib/percentage.ts.
pub const default_max_decimals: i32 = 2;
/// 2^-52 — identical to JavaScript's Number.EPSILON (std.math.floatEps(f64)).
const js_epsilon: f64 = std.math.floatEps(f64);
/// Options governing result precision. null means "omitted" → the default,
/// keeping an explicit 0 distinct from an unset value (the TS optional field).
pub const PercentOptions = struct {
max_decimals: ?i32 = null,
};
/// Effective precision: the explicit option when set, otherwise the default.
/// Mirrors the TS default-parameter behavior of round().
fn resolveMaxDecimals(opts: PercentOptions) i32 {
return opts.max_decimals orelse default_max_decimals;
}
/// Both finite (not NaN, not ±inf)? The TS everyFinite(...vals) twin — every
/// public function below takes exactly two numbers, so two slots suffice.
fn everyFinite(a: f64, b: f64) bool {
return std.math.isFinite(a) and std.math.isFinite(b);
}
/// Round to at most `max_decimals` places, absorbing binary float noise via
/// the epsilon nudge (the classic 0.005 → 0.01 case). Non-finite values pass
/// through unchanged so this function is total, exactly like the TS round().
pub fn round(n: f64, max_decimals: i32) f64 {
if (!std.math.isFinite(n)) return n;
const factor = std.math.pow(f64, 10, @floatFromInt(max_decimals));
// @floor(x + 0.5) replicates JavaScript's Math.round (half toward +inf) —
// NOT @round, which sends .5 ties away from zero.
return @floor((n + js_epsilon) * factor + 0.5) / factor;
}
/// X% of `value`: pct / 100 * value. null (the Zig analogue of the TS null)
/// when either input is non-finite.
pub fn percentOf(pct: f64, value: f64, opts: PercentOptions) ?f64 {
if (!everyFinite(pct, value)) return null;
return round((pct / 100) * value, resolveMaxDecimals(opts));
}
/// What percentage `part` is of `total`: part / total * 100. null when total
/// is 0 (undefined ratio) or either input is non-finite.
pub fn whatPercent(part: f64, total: f64, opts: PercentOptions) ?f64 {
if (!everyFinite(part, total) or total == 0) return null;
return round((part / total) * 100, resolveMaxDecimals(opts));
}
/// Percentage change from `from` to `to`, measured against |from| so the
/// result's sign reflects only the direction of change. null when `from` is
/// 0 (no meaningful base) or either input is non-finite.
pub fn percentChange(from: f64, to: f64, opts: PercentOptions) ?f64 {
if (!everyFinite(from, to) or from == 0) return null;
return round(((to - from) / @abs(from)) * 100, resolveMaxDecimals(opts));
}
/// Demo helper: print "null" for the undefined result, integers without a
/// trailing ".0" ({d} keeps the minimal representation).
fn show(label: []const u8, v: ?f64) void {
const stdout = std.io.getStdOut().writer();
if (v) |n|
stdout.print("{s:<24} = {d}\n", .{ label, n }) catch {}
else
stdout.print("{s:<24} = null\n", .{label}) catch {};
}
pub fn main() void {
const omitted = PercentOptions{}; // default → 2 decimals
const zero = PercentOptions{ .max_decimals = 0 }; // explicit 0 ≠ omitted
show("15% of 200", percentOf(15, 200, omitted));
show("30 is what % of 200", whatPercent(30, 200, omitted));
show("change 40 -> 50", percentChange(40, 50, omitted));
show("change 50 -> 40", percentChange(50, 40, omitted));
show("15% of 37, 0 decimals", percentOf(15, 37, zero));
show("30 is what % of 0", whatPercent(30, 0, omitted));
show("change 0 -> 50", percentChange(0, 50, omitted));
}
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 →