RATE = 0.07
#| $t_2 = \frac{\ln 2}{\ln(1 + r)}$: solve $(1+r)^t = 2$ by taking logs.
def doubling_time(r) {
"Years for growth at rate r per year to double: ln 2 / ln(1 + r)."
return log(2.0) / log(1.0 + r)
}
#| $70 / (100r)$: the same thing with $\ln 2 \approx 0.7$ and $\ln(1+r) \approx r$.
def rule_of_70(r) {
return 70.0 / (100.0 * r)
}
#| $72 / (100r)$: the version for mental arithmetic, since 72 divides by 2, 3, 4, 6, 8, 9 and 12.
def rule_of_72(r) {
return 72.0 / (100.0 * r)
}
#| How far a rule is from the exact doubling time, as a fraction of it.
def miss(rule_value, r) {
return abs(rule_value - doubling_time(r)) / doubling_time(r)
}
t_double = doubling_time(RATE)
show t_double
t_rule_of_70 = rule_of_70(RATE)
show t_rule_of_70
# the rate where the two rules miss by the same amount, by bisection
lo = 0.03
hi = 0.07
# halvings of the bracket: 60 take its width of 0.04 below double precision
BISECTIONS = 60
for step to BISECTIONS {
mid = 0.5 * (lo + hi)
if miss(rule_of_70(mid), mid) < miss(rule_of_72(mid), mid) {
lo = mid
}
else {
hi = mid
}
}
crossover = lo
show crossover
# a point every quarter year for twenty years
N_TIMES = 81
ts = linspace(0.0, 20.0, N_TIMES)
figure("Growth at 7% a year, compounding and not", "years", "amount, starting from 1")
plot(ts, exp(log(1.0 + RATE) * ts), "$1.07^t$")
plot(ts, 1.0 + RATE * ts, "1 + 0.07 t")
test "at 7% a year the doubling time is 10.24 years" {
assert_near(t_double, 10.2448, 1.0e-3)
}
test "after that long the amount is exactly 2" {
assert_near(pow(1.0 + RATE, t_double), 2.0, 1.0e-12)
}
test "the rule of 70 is 2.4% off at 7% and 0.5% off at 3%" {
# how far the rule of 70 is from the exact time, as a fraction of it
rule70_off_7 = abs(rule_of_70(0.07) - doubling_time(0.07)) / doubling_time(0.07)
rule70_off_3 = abs(rule_of_70(0.03) - doubling_time(0.03)) / doubling_time(0.03)
assert_near(rule70_off_7, 0.024, 0.0005)
assert_near(rule70_off_3, 0.005, 0.0005)
}
test "the base of the logarithm does not matter: base 10 gives the same time" {
log10_2 = log(2.0) / log(10.0)
log10_r = log(1.0 + RATE) / log(10.0)
assert_near(log10_2 / log10_r, t_double, 1.0e-12)
}
test "at 8% the rule of 72 is off by 0.07%" {
assert_near(miss(rule_of_72(0.08), 0.08), 0.0007, 0.00005)
}
test "the exact time times 100r is 69.3 as r goes to 0, 70.3 at 3% and 72.1 at 8%" {
# each to half a unit of its last stated digit
assert_near(100.0 * log(2.0), 69.3, 0.05)
assert_near(100.0 * 0.03 * doubling_time(0.03), 70.3, 0.05)
assert_near(100.0 * 0.08 * doubling_time(0.08), 72.1, 0.05)
}
test "72 divides by 2, 3, 4, 6, 8, 9 and 12" {
divisors = [2.0, 3.0, 4.0, 6.0, 8.0, 9.0, 12.0]
for k to len(divisors) {
quotient = 72.0 / divisors[k]
assert_near(quotient, floor(quotient), 0.0)
}
}
test "at 3% the rule of 70 is the closer one, at 7% the rule of 72" {
assert_lt(miss(rule_of_70(0.03), 0.03), miss(rule_of_72(0.03), 0.03))
assert_lt(miss(rule_of_72(0.07), 0.07), miss(rule_of_70(0.07), 0.07))
}
test "the two rules miss by the same amount at 4.90% a year" {
assert_near(crossover, 0.049018, 1.0e-5)
assert_near(miss(rule_of_70(crossover), crossover), miss(rule_of_72(crossover), crossover), 1.0e-9)
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: 2026 The Viv authors