finance 0.1.1 → 0.2.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.
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