finance 1.1.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY CHANGED
@@ -1,3 +1,11 @@
1
+ = Version 2.0.0
2
+ 23 Jul 2013
3
+
4
+ * Removed Integer#months, Integer#years, and replaced Numeric#to_d by Numeric#to_s in the interest of Rails compatibility.
5
+ * Converted unit tests from the shoulda framework to minitest.
6
+ * Removed octal numbers in test_cashflow.rb
7
+ * Thanks to @thadd, @bramswenson, and @xpe for their contributions to this release!
8
+
1
9
  = Version 1.1.2
2
10
  16 Jun 2012
3
11
 
@@ -1,104 +1,115 @@
1
- = FINANCE
1
+ # FINANCE
2
2
 
3
3
  a library for financial modelling in Ruby.
4
4
 
5
- == INSTALL
5
+ ## INSTALL
6
6
 
7
- $ sudo gem install finance
7
+ $ sudo gem install finance
8
8
 
9
- == OVERVIEW
9
+ ## IMPORTANT CHANGES
10
10
 
11
- === GETTING STARTED
11
+ Contributions by [@thadd](https://github.com/thadd) and
12
+ [@bramswenson](https://github.com/bramswenson) have made the `finance`
13
+ library fully compatible with rails, at the cost of the `#years` and
14
+ `#months` convenience methods on `Integer`, as well as the `#to_d` method for
15
+ converting `Numerics` into `DecNums`. These methods have been removed, due to
16
+ conflicts with existing rails methods.
12
17
 
13
- >> require 'finance'
18
+ Correspondingly, `finance` has been bumped up to version 2.0.
19
+
20
+ ## OVERVIEW
21
+
22
+ ### GETTING STARTED
23
+
24
+ >> require 'finance'
14
25
 
15
26
  *Note:* As of version 1.0.0, the entire library is contained under the
16
27
  Finance namespace. Existing code will not work unless you add:
17
28
 
18
- >> include Finance
29
+ >> include Finance
19
30
 
20
31
  for all of the examples below, we'll assume that you have done this.
21
32
 
22
- === AMORTIZATION
33
+ ### AMORTIZATION
23
34
 
24
35
  You are interested in borrowing $250,000 under a 30 year, fixed-rate
25
36
  loan with a 4.25% APR.
26
37
 
27
- >> rate = Rate.new(0.0425, :apr, :duration => 30.years)
28
- >> amortization = 250000.amortize(rate)
38
+ >> rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
39
+ >> amortization = Amortization.new(250000, rate)
29
40
 
30
41
  Find the standard monthly payment:
31
42
 
32
- >> amortization.payment
33
- => DecNum('-1229.91')
43
+ >> amortization.payment
44
+ => DecNum('-1229.91')
34
45
 
35
46
  Find the total cost of the loan:
36
47
 
37
- >> amortization.payments.sum
38
- => DecNum('-442766.55')
48
+ >> amortization.payments.sum
49
+ => DecNum('-442766.55')
39
50
 
40
51
  How much will you pay in interest?
41
52
 
42
- >> amortization.interest.sum
43
- => DecNum('192766.55')
53
+ >> amortization.interest.sum
54
+ => DecNum('192766.55')
44
55
 
45
56
  How much interest in the first six months?
46
57
 
47
- >> amortization.interest[0,6].sum
48
- => DecNum('5294.62')
58
+ >> amortization.interest[0,6].sum
59
+ => DecNum('5294.62')
49
60
 
50
61
  If your loan has an adjustable rate, no problem. You can pass an
51
62
  arbitrary number of rates, and they will be used in the amortization.
52
63
  For example, we can look at an amortization of $250000, where the APR
53
64
  starts at 4.25%, and increases by 1% every five years.
54
65
 
55
- >> values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
56
- >> rates = values.collect { |value| Rate.new( value, :apr, :duration = 5.years ) }
57
- >> arm = Amortization.new(250000, *rates)
66
+ >> values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
67
+ >> rates = values.collect { |value| Rate.new( value, :apr, :duration => (5 * 12) }
68
+ >> arm = Amortization.new(250000, *rates)
58
69
 
59
70
  Since we are looking at an ARM, there is no longer a single "payment" value.
60
71
 
61
- >> arm.payment
62
- => nil
72
+ >> arm.payment
73
+ => nil
63
74
 
64
75
  But we can look at the different payments over time.
65
76
 
66
- >> arm.payments.uniq
67
- => [DecNum('-1229.85'), DecNum('-1360.41'), DecNum('-1475.65'), DecNum('-1571.07'), ... snipped ... ]
77
+ >> arm.payments.uniq
78
+ => [DecNum('-1229.85'), DecNum('-1360.41'), DecNum('-1475.65'), DecNum('-1571.07'), ... snipped ... ]
68
79
 
69
80
  The other methods previously discussed can be accessed in the same way:
70
81
 
71
- >> arm.interest.sum
72
- => DecNum('287515.45')
73
- >> arm.payments.sum
74
- => DecNum('-537515.45')
82
+ >> arm.interest.sum
83
+ => DecNum('287515.45')
84
+ >> arm.payments.sum
85
+ => DecNum('-537515.45')
75
86
 
76
87
  Last, but not least, you may pass a block when creating an Amortization
77
88
  which returns a modified monthly payment. For example, to increase your
78
89
  payment by $150, do:
79
90
 
80
- >> rate = Rate.new(0.0425, :apr, :duration => 30.years)
81
- >> extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
91
+ >> rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
92
+ >> extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
82
93
 
83
94
  Disregarding the block, we have used the same parameters as the first
84
95
  example. Notice the difference in the results:
85
96
 
86
- >> amortization.payments.sum
87
- => DecNum('-442745.98')
88
- >> extra_payments.payments.sum
89
- => DecNum('-400566.24')
90
- >> amortization.interest.sum
91
- => DecNum('192745.98')
92
- >> extra_payments.interest.sum
93
- => DecNum('150566.24')
97
+ >> amortization.payments.sum
98
+ => DecNum('-442745.98')
99
+ >> extra_payments.payments.sum
100
+ => DecNum('-400566.24')
101
+ >> amortization.interest.sum
102
+ => DecNum('192745.98')
103
+ >> extra_payments.interest.sum
104
+ => DecNum('150566.24')
94
105
 
95
106
  You can also increase your payment to a specific amount:
96
107
 
97
- >> extra_payments_2 = 250000.amortize(rate){ -1500 }
108
+ >> extra_payments_2 = 250000.amortize(rate){ -1500 }
98
109
 
99
- == ABOUT
110
+ ## ABOUT
100
111
 
101
- I started developing _finance_ while analyzing mortgages as a personal
112
+ I started developing `finance` while analyzing mortgages as a personal
102
113
  project. Spreadsheets have convenient formulas for doing this type of
103
114
  work, until you want to do something semi-complex (like ARMs or extra
104
115
  payments), at which point you need to create your own amortization
@@ -110,31 +121,31 @@ have as a gem.
110
121
  More broadly, I believe there are many calculations that are necessary
111
122
  for the effective management of personal finances, but are difficult
112
123
  (or impossible) to do with spreadsheets or other existing open source
113
- tools. My hope is that the _finance_ library will grow to provide a set
124
+ tools. My hope is that the `finance` library will grow to provide a set
114
125
  of open, tested tools to fill this gap.
115
126
 
116
- If you have used _finance_ and find it useful, I would enjoy hearing
127
+ If you have used `finance` and find it useful, I would enjoy hearing
117
128
  about it!
118
129
 
119
- == FEATURES
130
+ ## FEATURES
120
131
 
121
132
  Currently implemented features include:
122
133
 
123
- * Uses the {http://flt.rubyforge.org/ flt} library to ensure precision decimal arithmetic in all calculations.
134
+ * Uses the [flt](http://flt.rubyforge.org/) library to ensure precision decimal arithmetic in all calculations.
124
135
  * Fixed-rate mortgage amortization (30/360).
125
136
  * Interest rates
126
137
  * Various cash flow computations, such as NPV and IRR.
127
138
  * Adjustable rate mortgage amortization.
128
139
  * Payment modifications (i.e., how does paying an additional $75 per month affect the amortization?)
129
140
 
130
- == RESOURCES
141
+ ## RESOURCES
131
142
 
132
- [RubyGems Page] {https://rubygems.org/gems/finance}
133
- [Source Code] {http://github.com/wkranec/finance}
134
- [Bug Tracker] {https://github.com/wkranec/finance/issues}
135
- [Google Group] {http://groups.google.com/group/finance-gem/topics?pli=1}
143
+ * [RubyGems Page](https://rubygems.org/gems/finance)
144
+ * [Source Code](http://github.com/wkranec/finance)
145
+ * [Bug Tracker](https://github.com/wkranec/finance/issues)
146
+ * [Google Group](http://groups.google.com/group/finance-gem/topics?pli=1)
136
147
 
137
- == COPYRIGHT
148
+ ## COPYRIGHT
138
149
 
139
150
  This library is released under the terms of the LGPL license.
140
151
 
@@ -1,5 +1,5 @@
1
+ require 'finance/decimal'
1
2
  require 'finance/cashflows'
2
- require 'finance/interval'
3
3
 
4
4
  # The *Finance* module adheres to the following conventions for
5
5
  # financial calculations:
@@ -8,14 +8,14 @@ module Finance
8
8
  # example uses the amortize method for the Numeric class. The second
9
9
  # calls Amortization.new directly.
10
10
  # @example Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR
11
- # rate = Rate.new(0.0425, :apr, :duration => 30.years)
11
+ # rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
12
12
  # amortization = 250000.amortize(rate)
13
13
  # @example Borrow $250,000 under a 30 year, adjustable rate loan, with an APR starting at 4.25%, and increasing by 1% every five years
14
14
  # values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
15
- # rates = values.collect { |value| Rate.new( value, :apr, :duration = 5.years ) }
15
+ # rates = values.collect { |value| Rate.new( value, :apr, :duration = (5 * 12) ) }
16
16
  # arm = Amortization.new(250000, *rates)
17
17
  # @example Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR, but pay $150 extra each month
18
- # rate = Rate.new(0.0425, :apr, :duration => 30.years)
18
+ # rate = Rate.new(0.0425, :apr, :duration => (5 * 12))
19
19
  # extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
20
20
  # @api public
21
21
  class Amortization
@@ -42,7 +42,7 @@ module Finance
42
42
 
43
43
  # @return [Array] the amount of any additional payments in each period
44
44
  # @example
45
- # rate = Rate.new(0.0375, :apr, :duration => 30.years)
45
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
46
46
  # amt = 300000.amortize(rate){ |payment| payment.amount-100}
47
47
  # amt.additional_payments #=> [DecNum('-100.00'), DecNum('-100.00'), ... ]
48
48
  # @api public
@@ -63,8 +63,8 @@ module Finance
63
63
 
64
64
  pmt = Payment.new(amount, :period => @period)
65
65
  if @block then pmt.modify(&@block) end
66
-
67
- rate.duration.times do
66
+
67
+ rate.duration.to_i.times do
68
68
  # Do this first in case the balance is zero already.
69
69
  if @balance.zero? then break end
70
70
 
@@ -78,7 +78,7 @@ module Finance
78
78
  if pmt.amount.abs > @balance then pmt.amount = -@balance end
79
79
  @transactions << pmt.dup
80
80
  @balance += pmt.amount
81
-
81
+
82
82
  @period += 1
83
83
  end
84
84
  end
@@ -111,11 +111,11 @@ module Finance
111
111
 
112
112
  # @return [Integer] the time required to pay off the loan, in months
113
113
  # @example In most cases, the duration is equal to the total duration of all rates
114
- # rate = Rate.new(0.0375, :apr, :duration => 30.years)
114
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
115
115
  # amt = 300000.amortize(rate)
116
116
  # amt.duration #=> 360
117
117
  # @example Extra payments may reduce the duration
118
- # rate = Rate.new(0.0375, :apr, :duration => 30.years)
118
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
119
119
  # amt = 300000.amortize(rate){ |payment| payment.amount-100}
120
120
  # amt.duration #=> 319
121
121
  # @api public
@@ -130,10 +130,10 @@ module Finance
130
130
  # @param [Proc] block
131
131
  # @api public
132
132
  def initialize(principal, *rates, &block)
133
- @principal = principal.to_d
133
+ @principal = Flt::DecNum.new(principal.to_s)
134
134
  @rates = rates
135
135
  @block = block
136
-
136
+
137
137
  # compute the total duration from all of the rates.
138
138
  @periods = (rates.collect { |r| r.duration }).sum
139
139
  @period = 0
@@ -148,11 +148,11 @@ module Finance
148
148
 
149
149
  # @return [Array] the amount of interest charged in each period
150
150
  # @example find the total cost of interest for a loan
151
- # rate = Rate.new(0.0375, :apr, :duration => 30.years)
151
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
152
152
  # amt = 300000.amortize(rate)
153
153
  # amt.interest.sum #=> DecNum('200163.94')
154
154
  # @example find the total interest charges in the first six months
155
- # rate = Rate.new(0.0375, :apr, :duration => 30.years)
155
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
156
156
  # amt = 300000.amortize(rate)
157
157
  # amt.interest[0,6].sum #=> DecNum('5603.74')
158
158
  # @api public
@@ -166,7 +166,7 @@ module Finance
166
166
  # @param [Integer] periods the number of periods needed for repayment
167
167
  # @note in most cases, you will probably want to use rate.monthly when calling this function outside of an Amortization instance.
168
168
  # @example
169
- # rate = Rate.new(0.0375, :apr, :duration => 30.years)
169
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
170
170
  # rate.duration #=> 360
171
171
  # Amortization.payment(200000, rate.monthly, rate.duration) #=> DecNum('-926.23')
172
172
  # @see http://en.wikipedia.org/wiki/Amortization_calculator
@@ -177,7 +177,7 @@ module Finance
177
177
 
178
178
  # @return [Array] the amount of the payment in each period
179
179
  # @example find the total payments for a loan
180
- # rate = Rate.new(0.0375, :apr, :duration => 30.years)
180
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
181
181
  # amt = 300000.amortize(rate)
182
182
  # amt.payments.sum #=> DecNum('-500163.94')
183
183
  # @api public
@@ -191,6 +191,6 @@ class Numeric
191
191
  # @see Amortization#new
192
192
  # @api public
193
193
  def amortize(*rates, &block)
194
- amortization = Finance::Amortization.new(self, *rates, &block)
194
+ Finance::Amortization.new(self, *rates, &block)
195
195
  end
196
196
  end
@@ -32,7 +32,7 @@ module Finance
32
32
  end
33
33
 
34
34
  def values(x)
35
- value = @transactions.send(@function, x[0].to_d)
35
+ value = @transactions.send(@function, Flt::DecNum.new(x[0].to_s))
36
36
  [ BigDecimal.new(value.to_s) ]
37
37
  end
38
38
  end
@@ -52,7 +52,7 @@ module Finance
52
52
 
53
53
  func = Function.new(self, :npv)
54
54
  rate = [ func.one ]
55
- n = nlsolve( func, rate )
55
+ nlsolve( func, rate )
56
56
  rate[0]
57
57
  end
58
58
 
@@ -69,9 +69,9 @@ module Finance
69
69
  # @see http://en.wikipedia.org/wiki/Net_present_value
70
70
  # @api public
71
71
  def npv(rate)
72
- self.collect! { |entry| entry.to_d }
72
+ self.collect! { |entry| Flt::DecNum.new(entry.to_s) }
73
73
 
74
- rate, total = rate.to_d, 0.to_d
74
+ rate, total = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(0.to_s)
75
75
  self.each_with_index do |cashflow, index|
76
76
  total += cashflow / (1 + rate) ** index
77
77
  end
@@ -97,7 +97,7 @@ module Finance
97
97
 
98
98
  func = Function.new(self, :xnpv)
99
99
  rate = [ func.one ]
100
- n = nlsolve( func, rate )
100
+ nlsolve( func, rate )
101
101
  Rate.new(rate[0], :apr, :compounds => :annually)
102
102
  end
103
103
 
@@ -111,11 +111,11 @@ module Finance
111
111
  # @transactions.xnpv(0.6).round(2) #=> -937.41
112
112
  # @api public
113
113
  def xnpv(rate)
114
- rate = rate.to_d
114
+ rate = Flt::DecNum.new(rate.to_s)
115
115
  start = self[0].date
116
116
 
117
117
  self.inject(0) do |sum, t|
118
- n = t.amount / ( (1 + rate) ** ((t.date-start) / 31536000.to_d)) # 365 * 86400
118
+ n = t.amount / ( (1 + rate) ** ((t.date-start) / Flt::DecNum.new(31536000.to_s))) # 365 * 86400
119
119
  sum + n
120
120
  end
121
121
  end
@@ -11,7 +11,7 @@ module Finance
11
11
  TYPES = { :apr => "effective",
12
12
  :apy => "effective",
13
13
  :effective => "effective",
14
- :nominal => "nominal"
14
+ :nominal => "nominal"
15
15
  }
16
16
 
17
17
  # @return [Integer] the duration for which the rate is valid, in months
@@ -55,13 +55,13 @@ module Finance
55
55
  # @api private
56
56
  def compounds=(input)
57
57
  @periods = case input
58
- when :annually then Flt::DecNum 1
58
+ when :annually then Flt::DecNum.new(1)
59
59
  when :continuously then Flt::DecNum.infinity
60
- when :daily then Flt::DecNum 365
61
- when :monthly then Flt::DecNum 12
62
- when :quarterly then Flt::DecNum 4
63
- when :semiannually then Flt::DecNum 2
64
- when Numeric then Flt::DecNum input.to_s
60
+ when :daily then Flt::DecNum.new(365)
61
+ when :monthly then Flt::DecNum.new(12)
62
+ when :quarterly then Flt::DecNum.new(4)
63
+ when :semiannually then Flt::DecNum.new(2)
64
+ when Numeric then Flt::DecNum.new(input.to_s)
65
65
  else raise ArgumentError
66
66
  end
67
67
  end
@@ -98,7 +98,7 @@ module Finance
98
98
 
99
99
  # Set the rate in the proper way, based on the value of type.
100
100
  begin
101
- send("#{TYPES.fetch(type)}=", rate.to_d)
101
+ send("#{TYPES.fetch(type)}=", Flt::DecNum.new(rate.to_s))
102
102
  rescue KeyError
103
103
  raise ArgumentError, "type must be one of #{TYPES.keys.join(', ')}", caller
104
104
  end
@@ -135,7 +135,7 @@ module Finance
135
135
  # Rate.to_effective(0.05, 4) #=> DecNum('0.05095')
136
136
  # @api public
137
137
  def Rate.to_effective(rate, periods)
138
- rate, periods = rate.to_d, periods.to_d
138
+ rate, periods = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(periods.to_s)
139
139
 
140
140
  if periods.infinite?
141
141
  rate.exp - 1
@@ -153,7 +153,7 @@ module Finance
153
153
  # @see http://www.miniwebtool.com/nominal-interest-rate-calculator/
154
154
  # @api public
155
155
  def Rate.to_nominal(rate, periods)
156
- rate, periods = rate.to_d, periods.to_d
156
+ rate, periods = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(periods.to_s)
157
157
 
158
158
  if periods.infinite?
159
159
  (rate + 1).log
@@ -24,7 +24,7 @@ module Finance
24
24
  # t.amount #=> 750
25
25
  # @api public
26
26
  def amount=(value)
27
- @amount = value.to_d
27
+ @amount = Flt::DecNum.new(value.to_s)
28
28
  end
29
29
 
30
30
  # @return [DecNum] the difference between the original transaction
@@ -51,7 +51,7 @@ module Finance
51
51
  def initialize(amount, opts={})
52
52
  @amount = amount
53
53
  @original = amount
54
-
54
+
55
55
  # Set optional attributes..
56
56
  opts.each do |key, value|
57
57
  send("#{key}=", value)
@@ -111,7 +111,7 @@ module Finance
111
111
  "Interest(#{@amount})"
112
112
  end
113
113
  end
114
-
114
+
115
115
  # Represent a loan payment as a Transaction
116
116
  # @see Transaction
117
117
  class Payment < Transaction
@@ -1,91 +1,84 @@
1
- require_relative '../lib/finance/amortization.rb'
2
- require_relative '../lib/finance/interval.rb'
3
- require_relative '../lib/finance/rates.rb'
4
- include Finance
5
-
6
- require 'flt/d'
7
- require 'minitest/unit'
8
- require 'shoulda'
1
+ require_relative 'test_helper'
9
2
 
10
3
  # @see http://tinyurl.com/6zroqvd for detailed calculations for the
11
4
  # examples in these unit tests.
12
- class TestAmortization < Test::Unit::TestCase
5
+ describe "Amortization" do
13
6
  def ipmt(principal, rate, payment, period)
14
7
  -(-rate*principal*(1+rate)**(period-1) - payment*((1+rate)**(period-1)-1)).round(2)
15
8
  end
16
9
 
17
- context "a fixed-rate amortization of 200000 at 3.75% over 30 years" do
18
- setup do
19
- @rate = Rate.new(0.0375, :apr, :duration => 30.years)
10
+ describe "a fixed-rate amortization of 200000 at 3.75% over 30 years" do
11
+ before(:all) do
12
+ @rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
20
13
  @principal = D(200000)
21
14
  @std = Amortization.new(@principal, @rate)
22
15
  end
23
16
 
24
- should "have a principal of $200,000" do
17
+ it "should have a principal of $200,000" do
25
18
  assert_equal @principal, @std.principal
26
19
  end
27
20
 
28
- should "have a final balance of zero" do
21
+ it "should have a final balance of zero" do
29
22
  assert @std.balance.zero?
30
23
  end
31
24
 
32
- should "have a duration of 360 months" do
25
+ it "should have a duration of 360 months" do
33
26
  assert_equal 360, @std.duration
34
27
  end
35
28
 
36
- should "have a monthly payment of $926.23" do
29
+ it "should have a monthly payment of $926.23" do
37
30
  assert_equal D('-926.23'), @std.payment
38
31
  end
39
32
 
40
- should "have a final payment of $926.96 (due to rounding)" do
33
+ it "should have a final payment of $926.96 (due to rounding)" do
41
34
  assert_equal D('-926.96'), @std.payments[-1]
42
35
  end
43
36
 
44
- should "have total payments of $333,443.53" do
37
+ it "should have total payments of $333,443.53" do
45
38
  assert_equal D('-333443.53'), @std.payments.sum
46
39
  end
47
40
 
48
- should "have interest charges which agree with the standard formula" do
41
+ it "should have interest charges which agree with the standard formula" do
49
42
  0.upto 359 do |period|
50
43
  assert_equal @std.interest[period], ipmt(@principal, @rate.monthly, @std.payment, period+1)
51
44
  end
52
45
  end
53
46
 
54
- should "have total interest charges of $133,433.33" do
47
+ it "should have total interest charges of $133,433.33" do
55
48
  assert_equal D('133443.53'), @std.interest.sum
56
49
  end
57
50
  end
58
51
 
59
- context "an adjustable rate amortization of 200000 starting at 3.75% and increasing by 1% every 3 years" do
60
- setup do
52
+ describe "an adjustable rate amortization of 200000 starting at 3.75% and increasing by 1% every 3 years" do
53
+ before(:all) do
61
54
  @rates = []
62
55
  0.upto 9 do |adj|
63
- @rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => 3.years)
56
+ @rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => (3 * 12))
64
57
  end
65
58
  @principal = D(200000)
66
59
  @arm = Amortization.new(@principal, *@rates)
67
60
  end
68
61
 
69
- should "have a principal of $200,000" do
62
+ it "should have a principal of $200,000" do
70
63
  assert_equal @principal, @arm.principal
71
64
  end
72
65
 
73
- should "have a final balance of zero" do
66
+ it "should have a final balance of zero" do
74
67
  assert @arm.balance.zero?
75
68
  end
76
69
 
77
- should "have a duration of 360 months" do
70
+ it "should have a duration of 360 months" do
78
71
  assert_equal 360, @arm.duration
79
72
  end
80
73
 
81
- should "not have a fixed monthly payment (since it changes)" do
74
+ it "should not have a fixed monthly payment (since it changes)" do
82
75
  assert_nil @arm.payment
83
76
  end
84
77
 
85
- should "have payments which increase every three years" do
78
+ it "should have payments which increase every three years" do
86
79
  values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 }
87
80
  values.collect!{ |v| -D(v) }
88
-
81
+
89
82
  payments = []
90
83
  values[0,9].each do |v|
91
84
  36.times do
@@ -100,70 +93,70 @@ class TestAmortization < Test::Unit::TestCase
100
93
  end
101
94
  end
102
95
 
103
- should "have a final payment of $1601.78 (due to rounding)" do
96
+ it "should have a final payment of $1601.78 (due to rounding)" do
104
97
  assert_equal D('-1601.78'), @arm.payments[-1]
105
98
  end
106
99
 
107
- should "have total payments of $47,505.92" do
100
+ it "should have total payments of $47,505.92" do
108
101
  assert_equal D('-477505.92'), @arm.payments.sum
109
102
  end
110
103
 
111
- should "have total interest charges of $277,505.92" do
104
+ it "should have total interest charges of $277,505.92" do
112
105
  assert_equal D('277505.92'), @arm.interest.sum
113
106
  end
114
107
  end
115
108
 
116
- context "a fixed-rate amortization of 200000 at 3.75% over 30 years, where an additional 100 is paid each month" do
117
- setup do
118
- @rate = Rate.new(0.0375, :apr, :duration => 30.years)
109
+ describe "a fixed-rate amortization of 200000 at 3.75% over 30 years, where an additional 100 is paid each month" do
110
+ before(:all) do
111
+ @rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
119
112
  @principal = D(200000)
120
113
  @exp = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
121
114
  end
122
115
 
123
- should "have a principal of $200,000" do
116
+ it "should have a principal of $200,000" do
124
117
  assert_equal @principal, @exp.principal
125
118
  end
126
119
 
127
- should "have a final balance of zero" do
120
+ it "should have a final balance of zero" do
128
121
  assert @exp.balance.zero?
129
122
  end
130
123
 
131
- should "have a duration of 301 months" do
124
+ it "should have a duration of 301 months" do
132
125
  assert_equal 301, @exp.duration
133
126
  end
134
127
 
135
- should "have a monthly payment of $1026.23" do
128
+ it "should have a monthly payment of $1026.23" do
136
129
  assert_equal D('-1026.23'), @exp.payment
137
130
  end
138
131
 
139
- should "have a final payment of $1011.09" do
132
+ it "should have a final payment of $1011.09" do
140
133
  assert_equal D('-1011.09'), @exp.payments[-1]
141
134
  end
142
135
 
143
- should "have total payments of $308,880.09" do
136
+ it "should have total payments of $308,880.09" do
144
137
  assert_equal D('-308880.09'), @exp.payments.sum
145
138
  end
146
139
 
147
- should "have total additional payments of $30,084.86" do
140
+ it "should have total additional payments of $30,084.86" do
148
141
  assert_equal D('-30084.86'), @exp.additional_payments.sum
149
142
  end
150
143
 
151
- should "have total interest charges of $108880.09" do
144
+ it "should have total interest charges of $108880.09" do
152
145
  assert_equal D('108880.09'), @exp.interest.sum
153
146
  end
154
147
  end
155
148
  end
156
149
 
157
- class TestNumericMethod < Test::Unit::TestCase
158
- def test_simple
159
- rate = Rate.new(0.0375, :apr, :duration => 30.years)
150
+ describe "Numeric Method" do
151
+ it 'works with simple invocation' do
152
+ rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
160
153
  amt_method = 300000.amortize(rate)
161
154
  amt_class = Amortization.new(300000, rate)
162
155
  assert_equal amt_method, amt_class
163
156
  end
164
157
 
165
- def test_with_block
166
- rate = Rate.new(0.0375, :apr, :duration => 30.years)
158
+ it 'works with block invocation' do
159
+ rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
167
160
  amt_method = 300000.amortize(rate){ |period| period.payment-300 }
168
161
  amt_class = Amortization.new(300000, rate){ |period| period.payment-300 }
169
162
  assert_equal amt_method, amt_class
@@ -1,37 +1,31 @@
1
- require_relative '../lib/finance/cashflows.rb'
2
- require_relative '../lib/finance/rates.rb'
3
- require_relative '../lib/finance/transaction.rb'
4
- include Finance
1
+ require_relative 'test_helper'
5
2
 
6
- require 'flt/d'
7
- require 'minitest/unit'
8
- require 'shoulda'
9
-
10
- class TestCashflows < Test::Unit::TestCase
11
- context "an array of numeric cashflows" do
12
- should "have an Internal Rate of Return" do
3
+ describe "Cashflows" do
4
+ describe "an array of numeric cashflows" do
5
+ it "should have an Internal Rate of Return" do
13
6
  assert_equal D("0.143"), [-4000,1200,1410,1875,1050].irr.round(3)
14
7
  assert_raises(ArgumentError) { [10,20,30].irr }
15
8
  end
16
9
 
17
- should "have a Net Present Value" do
10
+ it "should have a Net Present Value" do
18
11
  assert_equal D("49.211"), [-100.0, 60, 60, 60].npv(0.1).round(3)
19
12
  end
20
13
  end
21
- context "an array of Transactions" do
22
- setup do
14
+
15
+ describe "an array of Transactions" do
16
+ before(:all) do
23
17
  @xactions=[]
24
- @xactions << Transaction.new(-1000, :date => Time.new(1985,01,01))
25
- @xactions << Transaction.new( 600, :date => Time.new(1990,01,01))
26
- @xactions << Transaction.new( 600, :date => Time.new(1995,01,01))
18
+ @xactions << Transaction.new(-1000, :date => Time.new(1985, 1, 1))
19
+ @xactions << Transaction.new( 600, :date => Time.new(1990, 1, 1))
20
+ @xactions << Transaction.new( 600, :date => Time.new(1995, 1, 1))
27
21
  end
28
22
 
29
- should "have an Internal Rate of Return" do
23
+ it "should have an Internal Rate of Return" do
30
24
  assert_equal D("0.024851"), @xactions.xirr.effective.round(6)
31
- assert_raises(ArgumentError) { @xactions[1,2].xirr }
25
+ assert_raises(ArgumentError) { @xactions[1, 2].xirr }
32
26
  end
33
27
 
34
- should "have a Net Present Value" do
28
+ it "should have a Net Present Value" do
35
29
  assert_equal D("-937.41"), @xactions.xnpv(0.6).round(2)
36
30
  end
37
31
  end
@@ -0,0 +1,13 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/spec'
3
+
4
+ require 'active_support/all'
5
+
6
+ require 'flt'
7
+ require 'flt/d'
8
+
9
+ require_relative '../lib/finance/amortization.rb'
10
+ require_relative '../lib/finance/cashflows.rb'
11
+ require_relative '../lib/finance/rates.rb'
12
+ require_relative '../lib/finance/transaction.rb'
13
+ include Finance
@@ -1,77 +1,71 @@
1
- require_relative '../lib/finance/rates.rb'
2
- include Finance
1
+ require_relative 'test_helper'
3
2
 
4
- require 'flt'
5
- require 'flt/d'
6
- require 'minitest/unit'
7
- require 'shoulda'
8
-
9
- class TestRates < Test::Unit::TestCase
10
- context "an interest rate" do
11
- context "can compound with different periods" do
12
- should "compound monthly by default" do
3
+ describe "Rates" do
4
+ describe "an interest rate" do
5
+ describe "can compound with different periods" do
6
+ it "should compound monthly by default" do
13
7
  rate = Rate.new(0.15, :nominal)
14
8
  assert_equal D('0.16075'), rate.effective.round(5)
15
9
  end
16
10
 
17
- should "compound annually" do
11
+ it "should compound annually" do
18
12
  rate = Rate.new(0.15, :nominal, :compounds => :annually)
19
13
  assert_equal D('0.15'), rate.effective
20
14
  end
21
15
 
22
- should "compound continuously" do
16
+ it "should compound continuously" do
23
17
  rate = Rate.new(0.15, :nominal, :compounds => :continuously)
24
18
  assert_equal D('0.16183'), rate.effective.round(5)
25
19
  end
26
20
 
27
- should "compound daily" do
21
+ it "should compound daily" do
28
22
  rate = Rate.new(0.15, :nominal, :compounds => :daily)
29
23
  assert_equal D('0.16180'), rate.effective.round(5)
30
24
  end
31
25
 
32
- should "compound quarterly" do
26
+ it "should compound quarterly" do
33
27
  rate = Rate.new(0.15, :nominal, :compounds => :quarterly)
34
28
  assert_equal D('0.15865'), rate.effective.round(5)
35
29
  end
36
30
 
37
- should "compound semiannually" do
31
+ it "should compound semiannually" do
38
32
  rate = Rate.new(0.15, :nominal, :compounds => :semiannually)
39
33
  assert_equal D('0.15563'), rate.effective.round(5)
40
34
  end
41
35
 
42
- should "accept a numerical value as the compounding frequency per year" do
36
+ it "should accept a numerical value as the compounding frequency per year" do
43
37
  rate = Rate.new(0.15, :nominal, :compounds => 7)
44
38
  assert_equal D('0.15999'), rate.effective.round(5)
45
39
  end
46
40
 
47
- should "raise an exception if an unknown string is given" do
41
+ it "should raise an exception if an unknown string is given" do
48
42
  assert_raises(ArgumentError){ Rate.new(0.15, :nominal, :compounds => :quickly) }
49
43
  end
50
44
  end
51
45
 
52
- should "accept a duration if given" do
46
+ it "should accept a duration if given" do
53
47
  rate = Rate.new(0.0375, :effective, :duration => 360)
54
48
  assert_equal 360, rate.duration
55
49
  end
56
50
 
57
- should "be comparable to other interest rates" do
51
+ it "should be comparable to other interest rates" do
58
52
  r1 = Rate.new(0.15, :nominal)
59
53
  r2 = Rate.new(0.16, :nominal)
60
54
  assert_equal( 1, r2 <=> r1)
61
55
  assert_equal(-1, r1 <=> r2)
62
56
  end
63
-
64
- should "convert to a monthly value" do
57
+
58
+ it "should convert to a monthly value" do
65
59
  rate = Rate.new(0.0375, :effective)
66
60
  assert_equal D('0.003125'), rate.monthly
67
61
  end
68
62
 
69
- should "convert effective interest rates to nominal" do
63
+ it "should convert effective interest rates to nominal" do
70
64
  assert_equal D('0.03687'), Rate.to_nominal(D('0.0375'), 12).round(5)
71
65
  assert_equal D('0.03681'), Rate.to_nominal(D('0.0375'), Flt::DecNum.infinity).round(5)
72
66
  end
73
67
 
74
- should "raise an exception if an unknown value is given for :type" do
68
+ it "should raise an exception if an unknown value is given for :type" do
75
69
  assert_raises(ArgumentError){ Rate.new(0.0375, :foo) }
76
70
  end
77
71
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: finance
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 2.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-16 00:00:00.000000000 Z
12
+ date: 2013-07-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: flt
16
- requirement: &2152191000 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,33 +21,69 @@ dependencies:
21
21
  version: 1.3.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2152191000
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.3.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: minitest
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 4.7.5
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 4.7.5
46
+ - !ruby/object:Gem::Dependency
47
+ name: activesupport
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 4.0.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 4.0.0
25
62
  description: The finance library provides a Ruby interface for working with interest
26
63
  rates, mortgage amortization, and cashflows (NPV, IRR, etc.).
27
64
  email: wkranec@gmail.com
28
65
  executables: []
29
66
  extensions: []
30
67
  extra_rdoc_files:
31
- - README
68
+ - README.md
32
69
  - COPYING
33
70
  - COPYING.LESSER
34
71
  - HISTORY
35
72
  files:
36
- - README
73
+ - README.md
37
74
  - COPYING
38
75
  - COPYING.LESSER
39
76
  - HISTORY
77
+ - lib/finance.rb
78
+ - lib/finance/transaction.rb
40
79
  - lib/finance/amortization.rb
41
- - lib/finance/cashflows.rb
42
- - lib/finance/decimal.rb
43
- - lib/finance/interval.rb
44
80
  - lib/finance/rates.rb
45
- - lib/finance/transaction.rb
46
- - lib/finance.rb
81
+ - lib/finance/decimal.rb
82
+ - lib/finance/cashflows.rb
47
83
  - test/test_amortization.rb
48
84
  - test/test_cashflows.rb
49
- - test/test_interval.rb
50
85
  - test/test_rates.rb
86
+ - test/test_helper.rb
51
87
  homepage: https://rubygems.org/gems/finance
52
88
  licenses: []
53
89
  post_install_message:
@@ -68,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
104
  version: '0'
69
105
  requirements: []
70
106
  rubyforge_project:
71
- rubygems_version: 1.8.15
107
+ rubygems_version: 1.8.24
72
108
  signing_key:
73
109
  specification_version: 3
74
110
  summary: a library for financial modelling in Ruby.
@@ -1,13 +0,0 @@
1
- class Integer
2
- # convert an integer value representing months (or years) into months
3
- # @return [Integer] the number of months
4
- # @example
5
- # 360.months #=> 360
6
- # 30.years #=> 360
7
- # @api public
8
- def method_missing(name, *args, &block)
9
- return self if name.to_s == "months"
10
- return self * 12 if name.to_s == "years"
11
- super
12
- end
13
- end
@@ -1,18 +0,0 @@
1
- require_relative '../lib/finance/interval.rb'
2
-
3
- require 'minitest/unit'
4
- require 'shoulda'
5
-
6
- class TestInterval < Test::Unit::TestCase
7
- context "a time interval" do
8
- context "can be created from an integer" do
9
- should "convert an integer into months" do
10
- assert_equal 360, 360.months
11
- end
12
-
13
- should "convert an integer into years" do
14
- assert_equal 360, 30.years
15
- end
16
- end
17
- end
18
- end