time_value 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/solver.rb +17 -15
- data/lib/time_value.rb +42 -32
- data/lib/time_value/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8cb8d84e2f74389800bec8f0d5bf9c8858dbecf5
|
4
|
+
data.tar.gz: 009f75557b934dd9cf8bd71ad9b8d83db6889d29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea2d1631b57c1d0b95df5886981c312c8c1bc2ed7a0a704b4c3e24391e7ab7e3d1074656347e1dd321b224746756f67f3cd5cf9c7060172dab0e67ba9c5b59c9
|
7
|
+
data.tar.gz: 95e053b40b686c0d961de9991648b448b810fe0f369536ba61716f99bb159aa8bcce1ccf7d37af7a926009677cb76b53a7d7309e20ab2700288673875644fa77
|
data/lib/solver.rb
CHANGED
@@ -1,40 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
class Solver
|
2
3
|
PRECISION = 3
|
3
|
-
INTERVAL = 10
|
4
|
+
INTERVAL = 10**(-1 * PRECISION)
|
4
5
|
MAX_ITERATIONS = 20
|
5
6
|
attr_reader :time_value, :goal
|
6
|
-
attr_accessor :lower_bound, :upper_bound, :upper_cap_met
|
7
|
+
attr_accessor :lower_bound, :upper_bound, :upper_cap_met, :iteration_count
|
7
8
|
|
8
|
-
def initialize(time_value:, lower_bound: 0.00, upper_bound: nil, guess:
|
9
|
+
def initialize(time_value:, lower_bound: 0.00, upper_bound: nil, guess: 10.00)
|
9
10
|
@upper_bound = upper_bound || guess
|
10
11
|
@lower_bound = lower_bound
|
11
12
|
@time_value = time_value.dup
|
12
13
|
@time_value.i = guess
|
13
14
|
@goal = time_value.fv
|
14
15
|
@upper_cap_met = false
|
16
|
+
@iteration_count = 0
|
15
17
|
end
|
16
18
|
|
17
19
|
def solve!
|
18
|
-
iteration_count
|
19
|
-
|
20
|
-
|
21
|
-
iteration_count += 1
|
22
|
-
begin
|
23
|
-
result = time_value.calc_fv
|
24
|
-
rescue FloatDomainError
|
25
|
-
return nil
|
26
|
-
end
|
27
|
-
adjust_bounds!(result)
|
20
|
+
while !within_range? && iteration_count < MAX_ITERATIONS
|
21
|
+
result = time_value.calc_fv
|
22
|
+
adjust_bounds!(result: result)
|
28
23
|
time_value.i = new_guess
|
24
|
+
self.iteration_count += 1
|
29
25
|
end
|
30
26
|
# TODO: This will not handle the case where the 20th iteration
|
31
27
|
# finds the solution
|
32
|
-
rate if iteration_count < MAX_ITERATIONS
|
28
|
+
return rate if iteration_count < MAX_ITERATIONS
|
29
|
+
rescue FloatDomainError
|
30
|
+
return nil
|
33
31
|
end
|
34
32
|
|
35
33
|
private
|
36
34
|
|
37
|
-
def
|
35
|
+
def within_range?
|
36
|
+
(upper_bound - lower_bound).round(PRECISION) <= INTERVAL
|
37
|
+
end
|
38
|
+
|
39
|
+
def adjust_bounds!(result:)
|
38
40
|
if result > goal
|
39
41
|
# interest rate too high
|
40
42
|
self.upper_cap_met = true
|
data/lib/time_value.rb
CHANGED
@@ -1,51 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'solver'
|
2
3
|
|
3
4
|
class TimeValue
|
4
5
|
attr_accessor :n, :i, :pv, :pmt, :fv
|
5
6
|
|
6
7
|
def initialize(n: 0, i: 0, pv: 0.0, pmt: 0.0, fv: 0.0)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
@n = n
|
9
|
+
@i = i.to_f
|
10
|
+
@pv = pv.to_f
|
11
|
+
@pmt = pmt.to_f
|
12
|
+
@fv = fv.to_f
|
12
13
|
end
|
13
14
|
|
14
|
-
#Base formula, ordinary annuity: -PV = [FV/((1+i)^n)] +
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
15
|
+
# Base formula, ordinary annuity: -PV = [FV/((1+i)^n)] +
|
16
|
+
# (PMT/i)*[1-(1/(1+i)^n)]
|
17
|
+
# rubocop:disable Metrics/AbcSize
|
18
|
+
def calc_pv
|
19
|
+
i = @i / 100.0
|
20
|
+
# Initial contribution
|
21
|
+
pvf = @fv / ((1 + i)**@n)
|
22
|
+
# Present value of annuity
|
23
|
+
pva = (@pmt / i) * (1 - (1 / ((1 + i)**@n)))
|
24
|
+
@pv = -1 * (pvf + pva)
|
25
|
+
# Round
|
26
|
+
@pv = (@pv * 100).round / 100.0
|
24
27
|
end
|
28
|
+
# rubocop:enable Metrics/AbcSize
|
25
29
|
|
26
|
-
def calc_fv
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
def calc_fv
|
31
|
+
i = @i / 100.0
|
32
|
+
# Growth of initial contribution
|
33
|
+
fvp = @pv * ((1 + i)**@n)
|
34
|
+
# Growth of payments
|
35
|
+
fva = @pmt * (((1 + i)**@n) - 1) / i
|
36
|
+
@fv = -1 * (fvp + fva)
|
37
|
+
# Round
|
38
|
+
@fv = (@fv * 100).round / 100.0
|
35
39
|
end
|
36
40
|
|
37
|
-
def calc_n
|
38
|
-
|
39
|
-
|
41
|
+
def calc_n
|
42
|
+
i = @i / 100.0
|
43
|
+
numerator = Math.log((@pmt - (i * @fv)) / (@pmt + (i * @pv)))
|
44
|
+
denominator = Math.log(1 + i)
|
45
|
+
@n = (numerator / denominator).round(0)
|
40
46
|
end
|
41
47
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
48
|
+
# rubocop:disable Metrics/AbcSize
|
49
|
+
def calc_pmt
|
50
|
+
i = @i / 100.0
|
51
|
+
numerator = (-i * (@fv + (@pv * ((1 + i)**n))))
|
52
|
+
denominator = (((1 + i)**n) - 1)
|
53
|
+
@pmt = numerator / denominator
|
54
|
+
@pmt = (@pmt * 100).round / 100.0
|
46
55
|
end
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
47
57
|
|
48
|
-
def calc_i
|
58
|
+
def calc_i
|
49
59
|
solver = Solver.new(time_value: self, lower_bound: 0.00, guess: 10.00)
|
50
60
|
solver.solve!
|
51
61
|
end
|
data/lib/time_value/version.rb
CHANGED