Skip to content

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 →