RATE = 0.07
#| $\ln(1 + x)$, accurate for small $x$: $x \ln u / (u - 1)$ with $u = 1 + x$ as rounded.
def ln_one_plus(x) {
u = 1.0 + x
if u == 1.0 {
return x
}
return log(u) * x / (u - 1.0)
}
#| Years to double at a yearly rate, with interest added once a year.
def doubling_time(rate) {
"How many years until the amount is twice what it started as; infinite if it never is."
if rate <= 0.0 {
return 1.0 / 0.0
}
return log(2.0) / ln_one_plus(rate)
}
#| The rule of 70: divide 70 by the rate written as a percentage.
def rule_of_70(rate) {
return 70.0 / (100.0 * rate)
}
#| Years to double when the yearly rate is split into `times` smaller additions.
def doubling_time_n(rate, times) {
"Each addition is rate/times; there are `times` of them a year."
return log(2.0) / (times * ln_one_plus(rate / times))
}
# the answers the notes quote
t_double = doubling_time(RATE)
show t_double
show rule_of_70(RATE)
t_monthly = doubling_time_n(RATE, 12.0)
show t_monthly
t_continuous = log(2.0) / RATE
show t_continuous
# the picture: growth that compounds, against growth that does not
# 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 "compounded once a year, the n-times formula gives the yearly doubling time to 1e-12" {
assert_near(doubling_time_n(RATE, 1.0), t_double, 1.0e-12)
}
test "compounded monthly at 7%, the money doubles in 9.931 years" {
assert_near(t_monthly, 9.93096, 1.0e-4)
assert_near(pow(1.0 + RATE / 12.0, 12.0 * t_monthly), 2.0, 1.0e-12)
}
test "more additions a year double it sooner, but never sooner than ln 2 / r" {
assert_lt(doubling_time_n(RATE, 4.0), doubling_time_n(RATE, 1.0))
assert_lt(doubling_time_n(RATE, 12.0), doubling_time_n(RATE, 4.0))
assert_lt(doubling_time_n(RATE, 365.0), doubling_time_n(RATE, 12.0))
assert_lt(t_continuous, doubling_time_n(RATE, 365.0))
}
test "ten thousand additions a year is ln 2 / r plus r/(2n) of it, the next term of the series" {
excess = doubling_time_n(RATE, 1.0e4) / t_continuous - 1.0
assert_near(excess, RATE / 2.0e4, 1.0e-9)
}
test "at a rate of one part in a trillion a year, the doubling time is ln 2 / r to one part in a billion" {
assert_near(doubling_time(1.0e-12) * 1.0e-12 / log(2.0), 1.0, 1.0e-9)
}
test "at a rate of 1e-16 a year the doubling time is finite, 6.9e15 years" {
assert_near(doubling_time(1.0e-16) * 1.0e-16 / log(2.0), 1.0, 1.0e-9)
assert_near(doubling_time(1.0e-16), 6.9e15, 0.05e15)
}
test "money shrinking 5% a year never doubles: the doubling time is infinite" {
assert_gt(doubling_time(-0.05), 1.0e300)
}
test "a billion additions a year double it in ln 2 / r, to one part in a billion" {
assert_near(doubling_time_n(RATE, 1.0e9) / t_continuous, 1.0, 1.0e-9)
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: 2026 The Viv authors