finance 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -12,7 +12,8 @@ _finance_ - a library for financial calculations in Ruby.
12
12
 
13
13
  == AMORTIZATION
14
14
 
15
- You are interested in borrowing $250,000 under a 30 year, fixed-rate loan with a 4.25% APR.
15
+ You are interested in borrowing $250,000 under a 30 year, fixed-rate
16
+ loan with a 4.25% APR.
16
17
 
17
18
  >> rate = Rate.new(0.0425, :apr, :duration => 30.years)
18
19
  >> amortization = 250000.amortize(rate)
@@ -37,11 +38,74 @@ How much interest in the first six months?
37
38
  >> amortization.interest[0,6].sum
38
39
  => DecNum('5294.62')
39
40
 
41
+ If your loan has an adjustable rate, no problem. You can pass an
42
+ arbitrary number of rates, and they will be used in the amortization.
43
+ For example, we can look at an amortization of $250000, where the APR
44
+ starts at 4.25%, and increases by 1% every five years.
45
+
46
+ >> values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
47
+ >> rates = values.collect { |value| Rate.new( value, :apr, :duration = 5.years ) }
48
+ >> arm = Amortization.new(250000, *rates)
49
+
50
+ Since we are looking at an ARM, there is no longer a single "payment" value.
51
+
52
+ >> arm.payment
53
+ => nil
54
+
55
+ But we can look at the different payments over time.
56
+
57
+ >> arm.payments.uniq
58
+ => [DecNum('-1229.85'), DecNum('-1360.41'), DecNum('-1475.65'), DecNum('-1571.07'), DecNum('-1641.34'), ... snipped ... ]
59
+
60
+ The other methods previously discussed can be accessed in the same way:
61
+
62
+ >> arm.interest.sum
63
+ => DecNum('287515.45')
64
+ >> arm.payments.sum
65
+ => DecNum('-537515.45')
66
+
67
+ Last, but not least, you may pass a block when creating an Amortization
68
+ which returns a modified monthly payment. For example, to increase your
69
+ payment by $150, do:
70
+
71
+ >> rate = Rate.new(0.0425, :apr, :duration => 30.years)
72
+ >> extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
73
+
74
+ Disregarding the block, we have used the same parameters as the first
75
+ example. Notice the difference in the results:
76
+
77
+ >> amortization.payments.sum
78
+ => DecNum('-442745.98')
79
+ >> extra_payments.payments.sum
80
+ => DecNum('-400566.24')
81
+ >> amortization.interest.sum
82
+ => DecNum('192745.98')
83
+ >> extra_payments.interest.sum
84
+ => DecNum('150566.24')
85
+
86
+ *Note*: you are _not_ allowed to modify a payment to pay _less_ than the
87
+ normally calculated payment.
88
+
89
+ You can also increase your payment to a specific amount:
90
+
91
+ >> extra_payments_2 = 250000.amortize(rate){ -1500 }
92
+
40
93
  = ABOUT
41
94
 
42
- I began developing _finance_ while writing a Ruby script for analyzing
43
- mortgages. I couldn't find an existing resource for these tools, I am
44
- hoping to save other folks some time by releasing what I have as a gem.
95
+ I started developing _finance_ while analyzing mortgages as a personal
96
+ project. Spreadsheets have convenient formulas for doing this type of
97
+ work, until you want to do something semi-complex (like ARMs or extra
98
+ payments), at which point you need to create your own amortization
99
+ table. I thought I could create a better interface for this type of
100
+ work in Ruby, and since I couldn't find an existing resource for these
101
+ tools, I am hoping to save other folks some time by releasing what I
102
+ have as a gem.
103
+
104
+ More broadly, I believe there are many calculations that are necessary
105
+ for the effective management of personal finances, but are difficult
106
+ (or impossible) to do with spreadsheets or other existing open source
107
+ tools. My hope is that the _finance_ library will grow to provide a set
108
+ of open, tested tools to fill this gap.
45
109
 
46
110
  If you have used _finance_ and find it useful, I would enjoy hearing
47
111
  about it!
@@ -50,16 +114,12 @@ about it!
50
114
 
51
115
  Currently implemented features include:
52
116
 
117
+ * Uses the {http://flt.rubyforge.org/ flt} library to ensure precision decimal arithmetic in all calculations.
53
118
  * Fixed-rate mortgage amortization (30/360).
54
119
  * Interest rates
55
120
  * Various cash flow computations, such as NPV, IRR, and sum.
56
-
57
- Planned features include:
58
-
59
121
  * Adjustable rate mortgage amortization.
60
122
  * Payment modifications (i.e., how does paying an additional $75 per month affect the amortization?)
61
- * Balloon payments.
62
- * Support for amortization methods other than 30/360.
63
123
 
64
124
  = RESOURCES
65
125
 
@@ -69,6 +129,9 @@ This gem and related documentation is available through
69
129
  Source code and bug tracking is available via
70
130
  {http://github.com/wkranec/finance github}.
71
131
 
132
+ Additional documentation is available on the
133
+ {https://github.com/wkranec/finance/wiki wiki}.
134
+
72
135
  = COPYRIGHT
73
136
 
74
137
  This library is released under the terms of the LGPL license.
data/lib/amortization.rb CHANGED
@@ -1,91 +1,160 @@
1
+ require 'rubygems'
1
2
  require 'cashflows'
3
+ require 'flt'
2
4
 
3
5
  class Period
4
- attr_accessor :payment
5
- attr_accessor :principal
6
- attr_accessor :rate
7
-
8
- # Return the remaining balance at the end of the period.
9
- def balance
10
- @principal + @payment + self.interest
11
- end
12
-
13
- def initialize(principal, rate, payment)
14
- @principal = principal
15
- @rate = rate
16
- @payment = payment
17
- end
18
-
19
- # Return the interest charged for the period.
20
- def interest
21
- (@principal * @rate).round(2)
22
- end
6
+ attr_accessor :payment
7
+ attr_accessor :principal
8
+ attr_accessor :rate
9
+
10
+ def additional_payment
11
+ @payment-@original_payment
12
+ end
13
+
14
+ # Return the remaining balance at the end of the period.
15
+ def balance
16
+ @principal + @payment + self.interest
17
+ end
18
+
19
+ def initialize(principal, rate, payment)
20
+ @principal = principal
21
+ @rate = rate
22
+ @payment = payment
23
+ @original_payment = payment
24
+ end
25
+
26
+ # Return the interest charged for the period.
27
+ def interest
28
+ (@principal * @rate).round(2)
29
+ end
30
+
31
+ def modify_payment(&modifier)
32
+ value = modifier.call(self)
33
+
34
+ # There's a chance that the block does not return a decimal.
35
+ unless value.class == Flt::DecNum
36
+ value = Flt::DecNum value.to_s
37
+ end
38
+
39
+ self.payment = value
40
+ end
41
+
42
+ def payment=(value)
43
+ total = @principal + self.interest
44
+ if value.abs > total
45
+ @payment = -total
46
+ else
47
+ @payment = value
48
+ end
49
+ end
23
50
  end
24
51
 
25
52
  class Amortization
26
- attr_accessor :balance
27
- attr_accessor :duration
28
- attr_accessor :payment
29
- attr_accessor :periods
30
- attr_accessor :principal
31
- attr_accessor :rate
32
-
33
- def compute(balance, rate)
34
- duration = @duration - @periods.length
35
- @payment = Amortization.payment balance, rate.monthly, duration
36
-
37
- rate.duration.times do
38
- if @payment > @balance
39
- @payment = @balance
40
- end
41
-
42
- period = Period.new(@balance, rate.monthly, @payment)
43
- @periods << period
44
- @balance = period.balance
45
-
46
- if @balance.zero?
47
- break
48
- end
49
- end
50
- end
51
-
52
- def initialize(principal, rate)
53
- @principal = principal
54
- @balance = principal
55
- @rate = rate
56
- @duration = rate.duration
57
- @periods = []
58
-
59
- compute(@balance, @rate)
60
-
61
- # Add any remaining balance due to rounding error to the last payment.
62
- unless @balance.zero?
63
- @periods[-1].payment -= @balance
64
- @balance = 0
65
- end
66
- end
67
-
68
- def inspect
69
- "Amortization.new(#{@principal}, #{@rate}"
70
- end
71
-
72
- def interest
73
- @periods.collect { |period| period.interest }
74
- end
53
+ attr_accessor :balance
54
+ attr_accessor :block
55
+ attr_accessor :rate_duration
56
+ attr_accessor :payment
57
+ attr_accessor :periods
58
+ attr_accessor :principal
59
+ attr_accessor :rates
60
+
61
+ def ==(amortization)
62
+ self.principal == amortization.principal and self.rates == amortization.rates and self.payments == amortization.payments
63
+ end
64
+
65
+ def additional_payments
66
+ @periods.collect{ |period| period.additional_payment }
67
+ end
68
+
69
+ def amortize(rate)
70
+ # For the purposes of calculating a payment, the relevant time
71
+ # period is the remaining number of periods in the loan, _not_ the
72
+ # duration of the rate itself.
73
+ duration = @rate_duration - @periods.length
74
+ payment = Amortization.payment @balance, rate.monthly, duration
75
+
76
+ rate.duration.times do
77
+ # Do this first in case the balance is zero already.
78
+ if @balance.zero?
79
+ break
80
+ end
81
+
82
+ period = Period.new(@balance, rate.monthly, payment)
83
+
84
+ if @block
85
+ period.modify_payment(&@block)
86
+ end
87
+
88
+ @periods << period
89
+ @balance = period.balance
90
+ end
91
+
92
+ if @rates.length == 1
93
+ @payment = self.payments[0]
94
+ else
95
+ @payment = nil
96
+ end
97
+ end
98
+
99
+ # Compute the amortization of the principal.
100
+ def compute
101
+ @balance = @principal
102
+ @periods = []
103
+
104
+ @rates.each do |rate|
105
+ amortize(rate)
106
+ end
107
+
108
+ # Add any remaining balance due to rounding error to the last payment.
109
+ unless @balance.zero?
110
+ @periods[-1].payment -= @balance
111
+ @balance = 0
112
+ end
113
+ end
114
+
115
+ def duration
116
+ @periods.length
117
+ end
118
+
119
+ def initialize(principal, *rates, &block)
120
+ @principal = principal
121
+ @rates = rates
122
+ @rate_duration = (rates.collect { |r| r.duration }).sum
123
+ @block = block
124
+
125
+ compute
126
+ end
127
+
128
+ def inspect
129
+ "Amortization.new(#{@principal}"
130
+ end
131
+
132
+ # Return an Array with the amount of interest charged in each period.
133
+ def interest
134
+ @periods.collect { |period| period.interest }
135
+ end
75
136
 
76
137
  # Return the periodic payment due on a loan, based on the
77
138
  #{http://en.wikipedia.org/wiki/Amortization_calculator amortization process}.
78
- def Amortization.payment(balance, rate, periods)
79
- -(balance * (rate + (rate / ((1 + rate) ** periods - 1)))).round(2)
80
- end
81
-
82
- def payments
83
- @periods.collect { |period| period.payment }
84
- end
139
+ def Amortization.payment(balance, rate, periods)
140
+ -(balance * (rate + (rate / ((1 + rate) ** periods - 1)))).round(2)
141
+ end
142
+
143
+ # Return an array with the payment amount for each period.
144
+ def payments
145
+ @periods.collect { |period| period.payment }
146
+ end
147
+
148
+ # "Pretty print" a text amortization table.
149
+ def pprint
150
+ @periods.each_with_index do |p, i|
151
+ puts "%03d $%9s %8s $%7s $%7s $%9s" % [i, p.principal, p.rate, p.payment, p.interest, p.balance]
152
+ end
153
+ end
85
154
  end
86
155
 
87
156
  class Numeric
88
- def amortize(rate)
89
- Amortization.new(self, rate)
90
- end
157
+ def amortize(rate, &block)
158
+ amortization = Amortization.new(self, rate, &block)
159
+ end
91
160
  end
data/lib/cashflows.rb CHANGED
@@ -1,33 +1,33 @@
1
1
  module Cashflow
2
- # Return the {http://en.wikipedia.org/wiki/Internal_rate_of_return
3
- # internal rate of return} for a given sequence of cashflows.
4
- def irr(iterations=100)
5
- rate = 1.0
6
- investment = self[0]
7
- for i in 1..iterations+1
8
- rate = rate * (1 - self.npv(rate) / investment)
9
- end
10
- rate
11
- end
2
+ # Return the {http://en.wikipedia.org/wiki/Internal_rate_of_return
3
+ # internal rate of return} for a given sequence of cashflows.
4
+ def irr(iterations=100)
5
+ rate = 1.0
6
+ investment = self[0]
7
+ for i in 1..iterations+1
8
+ rate = rate * (1 - self.npv(rate) / investment)
9
+ end
10
+ rate
11
+ end
12
12
 
13
- # Return the {http://en.wikipedia.org/wiki/Net_present_value net present value} of a sequence of cash flows given
14
- # the discount rate _rate_.
15
- def npv(rate)
16
- total = 0.0
17
- self.each_with_index do |cashflow, index|
18
- total = total + cashflow / (1+rate) ** index
19
- end
20
- total
21
- end
13
+ # Return the {http://en.wikipedia.org/wiki/Net_present_value net present value} of a sequence of cash flows given
14
+ # the discount rate _rate_.
15
+ def npv(rate)
16
+ total = 0.0
17
+ self.each_with_index do |cashflow, index|
18
+ total = total + cashflow / (1+rate) ** index
19
+ end
20
+ total
21
+ end
22
22
 
23
- def sum
24
- self.inject(:+)
25
- end
23
+ def sum
24
+ self.inject(:+)
25
+ end
26
26
 
27
- def xirr
28
- end
27
+ def xirr
28
+ end
29
29
  end
30
30
 
31
31
  class Array
32
- include Cashflow
32
+ include Cashflow
33
33
  end
data/lib/finance.rb CHANGED
@@ -12,52 +12,52 @@ require 'timespan'
12
12
  # * *rate* represents the interest rate _per period_.
13
13
  module Finance
14
14
 
15
- def Finance.payments(principal, rates)
16
- p_total = rates.inject(0) { |sum, n| sum + n[0] }
17
- p_current = 0
18
- payments = []
19
-
20
- rates.each do |periods, rate|
21
- payment = Finance.pmt(principal, rate, p_total-p_current)
22
-
23
- begin
24
- payment = payment.round(2)
25
- rescue ArgumentError
26
- payment = (payment * 100.0).round / 100.0
27
- end
28
-
29
- if block_given?
30
- payment = yield(payment)
31
- end
32
-
33
- periods.times do
34
- interest = principal * rate
35
-
36
- if payment > principal + interest
37
- payment = principal + interest
38
- end
39
-
40
- principal = principal + interest - payment
41
-
42
- begin
43
- principal = principal.round(2)
44
- rescue ArgumentError
45
- principal = (principal * 100.0).round / 100.0
46
- end
47
-
48
- payments << payment
49
- break if principal == 0
50
-
51
- p_current = p_current + 1
52
- end
53
- end
54
-
55
- payments
56
- end
57
-
58
- # Return the number of periods needed to pay off a loan with the
59
- # given payment.
60
- def Finance.nper(payment, rate, principal)
61
- -(Math.log(1-((principal/payment)*rate))) / Math.log(1+rate)
62
- end
15
+ def Finance.payments(principal, rates)
16
+ p_total = rates.inject(0) { |sum, n| sum + n[0] }
17
+ p_current = 0
18
+ payments = []
19
+
20
+ rates.each do |periods, rate|
21
+ payment = Finance.pmt(principal, rate, p_total-p_current)
22
+
23
+ begin
24
+ payment = payment.round(2)
25
+ rescue ArgumentError
26
+ payment = (payment * 100.0).round / 100.0
27
+ end
28
+
29
+ if block_given?
30
+ payment = yield(payment)
31
+ end
32
+
33
+ periods.times do
34
+ interest = principal * rate
35
+
36
+ if payment > principal + interest
37
+ payment = principal + interest
38
+ end
39
+
40
+ principal = principal + interest - payment
41
+
42
+ begin
43
+ principal = principal.round(2)
44
+ rescue ArgumentError
45
+ principal = (principal * 100.0).round / 100.0
46
+ end
47
+
48
+ payments << payment
49
+ break if principal == 0
50
+
51
+ p_current = p_current + 1
52
+ end
53
+ end
54
+
55
+ payments
56
+ end
57
+
58
+ # Return the number of periods needed to pay off a loan with the
59
+ # given payment.
60
+ def Finance.nper(payment, rate, principal)
61
+ -(Math.log(1-((principal/payment)*rate))) / Math.log(1+rate)
62
+ end
63
63
  end
data/lib/rates.rb CHANGED
@@ -2,88 +2,92 @@ require 'rubygems'
2
2
  require 'flt'
3
3
 
4
4
  class Rate
5
- attr_accessor :duration
6
- attr_accessor :periods
7
- attr_accessor :nominal
5
+ attr_accessor :duration
6
+ attr_accessor :periods
7
+ attr_accessor :nominal
8
+
9
+ def ==(rate)
10
+ self.nominal == rate.nominal and self.periods == rate.periods
11
+ end
8
12
 
9
13
  # Alias method for *effective*.
10
- def apr=(apr)
11
- self.effective = apr
12
- end
14
+ def apr=(apr)
15
+ self.effective = apr
16
+ end
13
17
 
14
- def apr
15
- self.effective
16
- end
18
+ def apr
19
+ self.effective
20
+ end
17
21
 
18
22
  # Alias method for *effective*.
19
- def apy=(apy)
20
- self.effective = apy
21
- end
22
-
23
- def apy
24
- self.effective
25
- end
26
-
27
- def effective=(rate)
28
- unless @periods == Flt::DecNum.infinity
29
- @nominal = @periods * ((1 + rate) ** (1 / @periods) - 1)
30
- else
31
- @nominal = Math.log(rate + 1)
32
- end
33
- end
34
-
35
- def effective
36
- unless @periods == Flt::DecNum.infinity
37
- (1 + @nominal / @periods) ** @periods - 1
38
- else
39
- @nominal.exp - 1
40
- end
41
- end
42
-
43
- def initialize(rate, type, opts={})
44
- # Make sure the rate is a decimal.
45
- unless rate.class == Flt::DecNum
46
- rate = Flt::DecNum rate.to_s
47
- end
48
-
49
- # Set the compounding interval.
50
- compounding = opts.fetch(:compounds, :monthly)
51
-
52
- translate = {
53
- :annually => Flt::DecNum(1),
54
- :continuously => Flt::DecNum.infinity,
55
- :daily => Flt::DecNum(365),
56
- :monthly => Flt::DecNum(12),
57
- :quarterly => Flt::DecNum(4),
58
- :semiannually => Flt::DecNum(2)
59
- }
60
-
61
- if translate.has_key? compounding
62
- @periods = translate.fetch compounding
63
- elsif compounding.kind_of? Numeric
64
- @periods = Flt::DecNum compounding.to_s
65
- end
66
-
67
- # Set the rate in the proper way, based on the value of :type:.
68
- if %w{apr apy effective}.include? type.to_s
69
- self.effective = rate
70
- else
71
- @nominal = rate
72
- end
73
-
74
- # Set the remainder of the attributes provided in :opts:.
75
- opts.each do |key, value|
76
- unless key == :compounds
77
- send("#{key}=", value)
78
- end
79
- end
80
- end
81
-
82
- def inspect
83
- "Rate.new(#{self.apr.round(6)}, :apr)"
84
- end
85
-
86
- def monthly
87
- (self.effective / 12).round(6)
88
- end
23
+ def apy=(apy)
24
+ self.effective = apy
25
+ end
26
+
27
+ def apy
28
+ self.effective
29
+ end
30
+
31
+ def effective=(rate)
32
+ unless @periods == Flt::DecNum.infinity
33
+ @nominal = @periods * ((1 + rate) ** (1 / @periods) - 1)
34
+ else
35
+ @nominal = Math.log(rate + 1)
36
+ end
37
+ end
38
+
39
+ def effective
40
+ unless @periods == Flt::DecNum.infinity
41
+ (1 + @nominal / @periods) ** @periods - 1
42
+ else
43
+ @nominal.exp - 1
44
+ end
45
+ end
46
+
47
+ def initialize(rate, type, opts={})
48
+ # Make sure the rate is a decimal.
49
+ unless rate.class == Flt::DecNum
50
+ rate = Flt::DecNum rate.to_s
51
+ end
52
+
53
+ # Set the compounding interval.
54
+ compounding = opts.fetch(:compounds, :monthly)
55
+
56
+ translate = {
57
+ :annually => Flt::DecNum(1),
58
+ :continuously => Flt::DecNum.infinity,
59
+ :daily => Flt::DecNum(365),
60
+ :monthly => Flt::DecNum(12),
61
+ :quarterly => Flt::DecNum(4),
62
+ :semiannually => Flt::DecNum(2)
63
+ }
64
+
65
+ if translate.has_key? compounding
66
+ @periods = translate.fetch compounding
67
+ elsif compounding.kind_of? Numeric
68
+ @periods = Flt::DecNum compounding.to_s
69
+ end
70
+
71
+ # Set the rate in the proper way, based on the value of :type:.
72
+ if %w{apr apy effective}.include? type.to_s
73
+ self.effective = rate
74
+ else
75
+ @nominal = rate
76
+ end
77
+
78
+ # Set the remainder of the attributes provided in :opts:.
79
+ opts.each do |key, value|
80
+ unless key == :compounds
81
+ send("#{key}=", value)
82
+ end
83
+ end
84
+ end
85
+
86
+ def inspect
87
+ "Rate.new(#{self.apr.round(6)}, :apr)"
88
+ end
89
+
90
+ def monthly
91
+ (self.effective / 12).round(15)
92
+ end
89
93
  end
data/lib/timespan.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  class Numeric
2
- def months
3
- self
4
- end
2
+ def months
3
+ self
4
+ end
5
5
 
6
- # Return the number of months.
7
- def years
8
- self * 12
9
- end
6
+ # Return the number of months.
7
+ def years
8
+ self * 12
9
+ end
10
10
  end
@@ -4,35 +4,168 @@ require 'flt/d'
4
4
  require 'test/unit'
5
5
 
6
6
  class TestBasicAmortization < Test::Unit::TestCase
7
- def setup
8
- @rate = Rate.new(0.0375, :apr, :duration => 30.years)
9
- @amortization = Amortization.new(D(200000), @rate)
10
- end
11
-
12
- def test_balance
13
- assert_equal D(0), @amortization.balance
14
- end
15
-
16
- def test_interest_sum
17
- assert_equal D('133443.53'), @amortization.interest.sum
18
- end
19
-
20
- def test_payment
21
- assert_equal D('-926.23'), @amortization.payment
22
- end
23
-
24
- def test_payments
25
- payments = [ D('-926.23') ] * @rate.duration
26
- # Account for rounding errors in last payment.
27
- payments[-1] = D('-926.96')
28
- assert_equal payments, @amortization.payments
29
- end
30
-
31
- def test_payments_sum
32
- assert_equal D('-333443.53'), @amortization.payments.sum
33
- end
34
-
35
- def test_principal
36
- assert_equal D(200000), @amortization.principal
37
- end
7
+ def interest_in_period(principal, rate, payment, period)
8
+ -(-rate*principal*(1+rate)**(period-1) - payment*((1+rate)**(period-1)-1)).round(2)
9
+ end
10
+
11
+ def setup
12
+ @rate = Rate.new(0.0375, :apr, :duration => 30.years)
13
+ @principal = D(200000)
14
+ @amortization = Amortization.new(@principal, @rate)
15
+ end
16
+
17
+ def test_balance
18
+ assert @amortization.balance.zero?
19
+ end
20
+
21
+ def test_duration
22
+ assert_equal 360, @amortization.duration
23
+ end
24
+
25
+ def test_interest
26
+ 0.upto 359 do |period|
27
+ assert_equal @amortization.interest[period], interest_in_period(@principal, @rate.monthly, @amortization.payment, period+1)
28
+ end
29
+ end
30
+
31
+ def test_interest_sum
32
+ assert_equal D('133443.53'), @amortization.interest.sum
33
+ end
34
+
35
+ def test_payment
36
+ assert_equal D('-926.23'), @amortization.payment
37
+ end
38
+
39
+ def test_payments
40
+ payments = [ D('-926.23') ] * @rate.duration
41
+ # Account for rounding errors in last payment.
42
+ payments[-1] = D('-926.96')
43
+ assert_equal payments, @amortization.payments
44
+ end
45
+
46
+ def test_payments_sum
47
+ assert_equal D('-333443.53'), @amortization.payments.sum
48
+ end
49
+
50
+ def test_principal
51
+ assert_equal @principal, @amortization.principal
52
+ end
53
+
54
+ def test_sum
55
+ assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
56
+ end
57
+ end
58
+
59
+ class TestAdjustableAmortization < Test::Unit::TestCase
60
+ def setup
61
+ @rates = []
62
+ 0.upto 9 do |adj|
63
+ @rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => 3.years)
64
+ end
65
+ @principal = D(200000)
66
+ @amortization = Amortization.new(@principal, *@rates)
67
+ end
68
+
69
+ def test_balance
70
+ assert @amortization.balance.zero?
71
+ end
72
+
73
+ def test_duration
74
+ assert_equal 360, @amortization.duration
75
+ end
76
+
77
+ def test_interest_sum
78
+ assert_equal D('277505.92'), @amortization.interest.sum
79
+ end
80
+
81
+ def test_payment
82
+ assert_nil @amortization.payment
83
+ end
84
+
85
+ def test_payments
86
+ values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 1601.78}
87
+ values.collect!{ |v| -D(v) }
88
+
89
+ payments = []
90
+ values[0,9].each do |v|
91
+ 36.times do
92
+ payments << v
93
+ end
94
+ end
95
+
96
+ 35.times { payments << values[9] }
97
+ payments << values[10]
98
+
99
+ payments.each_with_index do |payment, index|
100
+ assert_equal payment, @amortization.payments[index]
101
+ end
102
+ end
103
+
104
+ def test_payment_sum
105
+ assert_equal D('-477505.92'), @amortization.payments.sum
106
+ end
107
+
108
+ def test_principal
109
+ assert_equal @principal, @amortization.principal
110
+ end
111
+
112
+ def test_sum
113
+ assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
114
+ end
115
+ end
116
+
117
+ class TestExtraPaymentAmortization < Test::Unit::TestCase
118
+ def setup
119
+ @rate = Rate.new(0.0375, :apr, :duration => 30.years)
120
+ @principal = D(200000)
121
+ @amortization = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
122
+ end
123
+
124
+ def test_additional_payments_sum
125
+ assert_equal D('-30084.86'), @amortization.additional_payments.sum
126
+ end
127
+
128
+ def test_balance
129
+ assert @amortization.balance.zero?
130
+ end
131
+
132
+ def test_duration
133
+ assert_equal 301, @amortization.duration
134
+ end
135
+
136
+ def test_interest_sum
137
+ assert_equal D('108880.09'), @amortization.interest.sum
138
+ end
139
+
140
+ def test_payment
141
+ assert_equal D('-1026.23'), @amortization.payment
142
+ end
143
+
144
+ def test_payment_sum
145
+ assert_equal D('-308880.09'), @amortization.payments.sum
146
+ end
147
+
148
+ def test_principal
149
+ assert_equal @principal, @amortization.principal
150
+ end
151
+
152
+ def test_sum
153
+ assert_equal D(0), @amortization.payments.sum + @amortization.interest.sum + @amortization.principal
154
+ end
155
+ end
156
+
157
+ class TestNumericMethod < Test::Unit::TestCase
158
+ def test_simple
159
+ rate = Rate.new(0.0375, :apr, :duration => 30.years)
160
+ amt_method = 300000.amortize(rate)
161
+ amt_class = Amortization.new(300000, rate)
162
+ assert_equal amt_method, amt_class
163
+ end
164
+
165
+ def test_with_block
166
+ rate = Rate.new(0.0375, :apr, :duration => 30.years)
167
+ amt_method = 300000.amortize(rate){ |period| period.payment-300 }
168
+ amt_class = Amortization.new(300000, rate){ |period| period.payment-300 }
169
+ assert_equal amt_method, amt_class
170
+ end
38
171
  end
@@ -2,13 +2,13 @@ require 'cashflows'
2
2
  require 'test/unit'
3
3
 
4
4
  class TestIRR < Test::Unit::TestCase
5
- def test_simple
6
- assert_in_delta(0.143, [-4000,1200,1410,1875,1050].irr, 0.001)
7
- end
5
+ def test_simple
6
+ assert_in_delta(0.143, [-4000,1200,1410,1875,1050].irr, 0.001)
7
+ end
8
8
  end
9
9
 
10
10
  class TestNPV < Test::Unit::TestCase
11
- def test_simple
12
- assert_in_delta(49.211, [-100.0, 60, 60, 60].npv(0.1), 0.001)
13
- end
11
+ def test_simple
12
+ assert_in_delta(49.211, [-100.0, 60, 60, 60].npv(0.1), 0.001)
13
+ end
14
14
  end
data/test/test_rates.rb CHANGED
@@ -4,28 +4,28 @@ require 'flt/d'
4
4
  require 'test/unit'
5
5
 
6
6
  class TestRates < Test::Unit::TestCase
7
- def test_apr
8
- rate = Rate.new(0.0375, :apr)
9
- assert_equal D('0.03687'), rate.nominal.round(5)
10
- end
7
+ def test_apr
8
+ rate = Rate.new(0.0375, :apr)
9
+ assert_equal D('0.03687'), rate.nominal.round(5)
10
+ end
11
11
 
12
- def test_duration
13
- rate = Rate.new(0.0375, :effective, :duration => 30.years)
14
- assert_equal 360, rate.duration
15
- end
16
-
17
- def test_effective
18
- rate = Rate.new(0.03687, :nominal)
19
- assert_equal D('0.0375'), rate.effective.round(4)
20
- end
12
+ def test_duration
13
+ rate = Rate.new(0.0375, :effective, :duration => 30.years)
14
+ assert_equal 360, rate.duration
15
+ end
16
+
17
+ def test_effective
18
+ rate = Rate.new(0.03687, :nominal)
19
+ assert_equal D('0.0375'), rate.effective.round(4)
20
+ end
21
21
 
22
- def test_monthly
23
- rate = Rate.new(0.0375, :effective)
24
- assert_equal D('0.003125'), rate.monthly
25
- end
22
+ def test_monthly
23
+ rate = Rate.new(0.0375, :effective)
24
+ assert_equal D('0.003125'), rate.monthly
25
+ end
26
26
 
27
- def test_nominal
28
- rate = Rate.new(0.0375, :effective)
29
- assert_equal D('0.03687'), rate.nominal.round(5)
30
- end
27
+ def test_nominal
28
+ rate = Rate.new(0.0375, :effective)
29
+ assert_equal D('0.03687'), rate.nominal.round(5)
30
+ end
31
31
  end
@@ -2,11 +2,11 @@ require 'finance'
2
2
  require 'test/unit'
3
3
 
4
4
  class TestTime < Test::Unit::TestCase
5
- def test_months
6
- assert_equal 360, 360.months
7
- end
5
+ def test_months
6
+ assert_equal 360, 360.months
7
+ end
8
8
 
9
- def test_years
10
- assert_equal 360, 30.years
11
- end
9
+ def test_years
10
+ assert_equal 360, 30.years
11
+ end
12
12
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: finance
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Bill Kranec
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-06-21 00:00:00 Z
18
+ date: 2011-06-28 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: flt