Percentage Calculator — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# percentage-calculator — Ruby port: three-way percentage math (X% of Y, X as % of Y, % change).
# Rounding precision when an option is omitted, matching DEFAULT_MAX_DECIMALS
# in src/lib/percentage.ts.
DEFAULT_MAX_DECIMALS = 2
# 2^-52 — identical to JavaScript's Number.EPSILON.
EPSILON = 2.2204460492503131e-16
# 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 (NaN,
# Infinity) pass through unchanged, exactly like the TS round().
def round_value(n, max_decimals = DEFAULT_MAX_DECIMALS)
return n unless n.finite?
factor = 10.0**max_decimals
# (x + 0.5).floor replicates JavaScript's Math.round (half toward +infinity)
# — Ruby's Float#round sends .5 away from zero instead.
((n + EPSILON) * factor + 0.5).floor.to_f / factor
end
# X% of value: pct / 100 * value. nil (the Ruby analogue of the TS null) when
# either input is non-finite. Numeric inputs are coerced to Float so Integer
# args cannot trigger Ruby's floor division.
def percent_of(pct, value, max_decimals: nil)
pct = Float(pct)
value = Float(value)
return nil unless pct.finite? && value.finite?
round_value((pct / 100) * value, max_decimals || DEFAULT_MAX_DECIMALS)
end
# What percentage part is of total: part / total * 100. nil when total is 0
# (undefined ratio) or either input is non-finite.
def what_percent(part, total, max_decimals: nil)
part = Float(part)
total = Float(total)
return nil unless part.finite? && total.finite?
return nil if total.zero?
round_value((part / total) * 100, max_decimals || DEFAULT_MAX_DECIMALS)
end
# Percentage change from +from+ to +to+, measured against |from| so the
# result's sign reflects only the direction of change. nil when +from+ is 0
# (no meaningful base) or either input is non-finite.
def percent_change(from, to, max_decimals: nil)
from = Float(from)
to = Float(to)
return nil unless from.finite? && to.finite?
return nil if from.zero?
round_value(((to - from) / from.abs) * 100, max_decimals || DEFAULT_MAX_DECIMALS)
end
# Demo helper: print "null" for the undefined result, integers without a
# trailing ".0".
def show(label, value)
text = value.nil? ? 'null' : format('%g', value)
puts format('%-24s = %s', label, text)
end
omitted = {} # default -> 2 decimals
zero = { max_decimals: 0 } # explicit 0 != omitted
show('15% of 200', percent_of(15, 200, **omitted))
show('30 is what % of 200', what_percent(30, 200, **omitted))
show('change 40 -> 50', percent_change(40, 50, **omitted))
show('change 50 -> 40', percent_change(50, 40, **omitted))
show('15% of 37, 0 decimals', percent_of(15, 37, **zero))
show('30 is what % of 0', what_percent(30, 0, **omitted))
show('change 0 -> 50', percent_change(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 →