loan_creator 0.2.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 05b6daa216ea6263c492b90400a5885c6da2cf11
4
- data.tar.gz: 5f583406fd970409e5e0c5c471006600a3f019dd
3
+ metadata.gz: ce94717f0c667263653597ea2037844157e6cebe
4
+ data.tar.gz: e498fe9069ca0ff9c3d765083a182f28ddc3ca22
5
5
  SHA512:
6
- metadata.gz: bd2304c39a20bcc147c29f816be628f9cca6979763acb99287c49b526cf0ddb9120856ddec0f8e4ba3b16f431629e7749552a00997c3afeafd8f43e01a4a2cd9
7
- data.tar.gz: a03687f04b2a9718c84027aae35b30d0d37e7a11a189d7e64a05b9d9c5f8251c1987e7e80c49250e91f0e3ef73712c842a674975be960aac286f124836677832
6
+ metadata.gz: da8108111a04280719eba3143444bd2ceac35abaade6521c2e9a5a3297266037c7d8e194156c3a373602fe766bbce49c23ebe515a2db1b64e1a35ac67a4aff06
7
+ data.tar.gz: 5e49dd7114a25fc67e9230ea0d6120b7a12bb83b6b6c50d28eecbfb7c92cc7bbd68da4ca26d0bf7dffe0b7b1fc2a7ec9acbbbb9369bf2cdfb1eb6837625dd2be
data/CapSens_Loan.xlsx ADDED
Binary file
data/README.md CHANGED
@@ -46,6 +46,7 @@ Each instance of one of the previous classes has the following attributes:
46
46
  :starts_at
47
47
  :duration_in_periods
48
48
  :deferred_in_periods (default to zero)
49
+ :interests_start_date (optional)
49
50
  ```
50
51
 
51
52
  There is also a `LoanCreator::Timetable` class dedicated to record the data of the loans' terms. Each instance of `LoanCreator::Timetable` represents an array of `LoanCreator::Term` records, each having the following attributes:
@@ -55,7 +56,7 @@ There is also a `LoanCreator::Timetable` class dedicated to record the data of t
55
56
  :index
56
57
 
57
58
  # Term date
58
- :date
59
+ :due_on
59
60
 
60
61
  # Remaining due capital at the beginning of the term
61
62
  :crd_beginning_of_period
@@ -99,8 +100,29 @@ support all those differences.
99
100
  `.borrower_timetable(*lenders_timetables)` (class method) intends to sum each attribute of each provided `lender_timetable` on each term and thus to provide an ascending order array of `LoanCreator::Term`. It should be used for the borrower of a loan, once all lenders and their lending amounts
100
101
  are known. It makes the borrower support all financial rounding differences.
101
102
 
103
+ ## Example
104
+
105
+ ```ruby
106
+ loan_creator = LoanCreator::Standard.new(
107
+ period: :year,
108
+ amount: 42_000,
109
+ annual_interests_rate: 4,
110
+ starts_on: '2019-03-01',
111
+ duration_in_periods: 5,
112
+ deferred_in_periods: 1,
113
+ interests_start_date: '2019-02-10'
114
+ )
115
+ loan_creator.lender_timetable
116
+ # => #<LoanCreator::Timetable:0x0000000003198bd0 @terms=[...] ...>
117
+ loan_creator.lender_timetable.terms.first
118
+ # => #<LoanCreator::Term:0x00000000030f1a88 @crd_beginning_of_period=0.42e5,
119
+ # [...] @period_amount_to_pay=0.8745e2, @index=0, @due_on=Sun, 10 Feb 2019>
120
+ ````
121
+
102
122
  ## Explanation
103
123
 
124
+ ### Classes
125
+
104
126
  `Standard` loan generates terms with constant payments.
105
127
 
106
128
  `Linear` loan generates terms with constant capital share payment.
@@ -117,6 +139,26 @@ Capital share shall be repaid in full and all interests paid at loan's end.
117
139
 
118
140
  There is no deferred time for `InFine` and `Bullet` loans as it would be equivalent to increasing loan's duration.
119
141
 
142
+ ### Attributes
143
+
144
+ `period`: A `Symbol`. `:month`, `:quarter`, `:semester` or `:year`.
145
+
146
+ `duration_in_periods`: An `Integer`.
147
+
148
+ `amount`: Any number that can be converted to `BigDecimal`.
149
+
150
+ `annual_interests_rate`: In percentage. Any number that can be converted to `BigDecimal`.
151
+
152
+ `starts_at`: A `Date`, or a `String` that can be parsed.
153
+
154
+ `deferred_in_periods`: Optional. An `Integer`, smaller than `duration_in_periods`. Number of periods during which no
155
+ capital is refunded, only interest. Only relevant for `Standard` and `Linear` loans.
156
+
157
+ `interests_start_date`: Optional. To be used when the loan starts before the first full term date. This then compute an
158
+ additional term with only interests for the time difference.
159
+ For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
160
+ first term corresponding to 3 months of interests.
161
+
120
162
  ## Development
121
163
 
122
164
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -2,6 +2,7 @@ module LoanCreator
2
2
  class Bullet < LoanCreator::Common
3
3
  def lender_timetable
4
4
  raise ArgumentError.new(:deferred_in_periods) unless deferred_in_periods == 0
5
+ raise ArgumentError.new(:interests_start_date) unless interests_start_date.nil?
5
6
  timetable = new_timetable
6
7
  reset_current_term
7
8
  @crd_beginning_of_period = amount
@@ -19,7 +19,8 @@ module LoanCreator
19
19
 
20
20
  OPTIONAL_ATTRIBUTES = {
21
21
  # attribute: default_value
22
- deferred_in_periods: 0
22
+ deferred_in_periods: 0,
23
+ interests_start_date: nil,
23
24
  }.freeze
24
25
 
25
26
  attr_reader *REQUIRED_ATTRIBUTES
@@ -48,7 +49,7 @@ module LoanCreator
48
49
  end
49
50
 
50
51
  def self.bigd(value)
51
- BigDecimal.new(value, BIG_DECIMAL_DIGITS)
52
+ BigDecimal(value, BIG_DECIMAL_DIGITS)
52
53
  end
53
54
 
54
55
  def bigd(value)
@@ -65,7 +66,8 @@ module LoanCreator
65
66
  @options[:period] = @options[:period].to_sym
66
67
  @options[:amount] = bigd(@options[:amount])
67
68
  @options[:annual_interests_rate] = bigd(@options[:annual_interests_rate])
68
- @options[:starts_on] = @options[:starts_on].strftime('%Y-%m-%d') if Date === @options[:starts_on]
69
+ @options[:starts_on] = Date.parse(@options[:starts_on]) if @options[:starts_on].is_a?(String)
70
+ @options[:interests_start_date] = Date.parse(@options[:interests_start_date]) if @options[:interests_start_date].is_a?(String)
69
71
  end
70
72
 
71
73
  def set_attributes
@@ -83,7 +85,7 @@ module LoanCreator
83
85
  validate(:period) { |v| PERIODS_IN_MONTHS.keys.include?(v) }
84
86
  validate(:amount) { |v| v.is_a?(BigDecimal) && v > 0 }
85
87
  validate(:annual_interests_rate) { |v| v.is_a?(BigDecimal) && v >= 0 }
86
- validate(:starts_on) { |v| !!Date.parse(v) }
88
+ validate(:starts_on) { |v| v.is_a?(Date) }
87
89
  validate(:duration_in_periods) { |v| v.is_a?(Integer) && v > 0 }
88
90
  validate(:deferred_in_periods) { |v| v.is_a?(Integer) && v >= 0 && v < duration_in_periods }
89
91
  end
@@ -100,6 +102,8 @@ module LoanCreator
100
102
  @total_paid_capital_end_of_period = bigd('0')
101
103
  @total_paid_interests_end_of_period = bigd('0')
102
104
  @period_amount_to_pay = bigd('0')
105
+ @due_on = nil
106
+ @index = nil
103
107
  end
104
108
 
105
109
  def current_term
@@ -114,12 +118,46 @@ module LoanCreator
114
118
  period_capital: @period_capital,
115
119
  total_paid_capital_end_of_period: @total_paid_capital_end_of_period,
116
120
  total_paid_interests_end_of_period: @total_paid_interests_end_of_period,
117
- period_amount_to_pay: @period_amount_to_pay
121
+ period_amount_to_pay: @period_amount_to_pay,
122
+ due_on: @due_on,
123
+ index: @index
118
124
  )
119
125
  end
120
126
 
121
127
  def new_timetable
122
- LoanCreator::Timetable.new(starts_on: starts_on, period: period)
128
+ LoanCreator::Timetable.new(starts_on: starts_on, period: period, interests_start_date: interests_start_date)
129
+ end
130
+
131
+ def compute_term_zero
132
+ @crd_beginning_of_period = @crd_end_of_period
133
+ @period_theoric_interests = term_zero_interests
134
+ @delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
135
+ @accrued_delta_interests += @delta_interests
136
+ @period_interests = @period_theoric_interests.round(2)
137
+ @total_paid_interests_end_of_period += @period_interests
138
+ @period_amount_to_pay = @period_interests
139
+ @index = 0
140
+ end
141
+
142
+ def term_zero_interests
143
+ @crd_beginning_of_period * term_zero_interests_rate
144
+ end
145
+
146
+ def term_zero_interests_rate
147
+ term_zero_interests_rate_percentage = (annual_interests_rate * term_zero_duration).div(365, BIG_DECIMAL_DIGITS)
148
+ term_zero_interests_rate_percentage.div(100, BIG_DECIMAL_DIGITS)
149
+ end
150
+
151
+ def term_zero_duration
152
+ (term_zero_date - interests_start_date).to_i
153
+ end
154
+
155
+ def term_zero_date
156
+ starts_on.advance(months: -PERIODS_IN_MONTHS.fetch(@period))
157
+ end
158
+
159
+ def term_zero?
160
+ interests_start_date && interests_start_date < term_zero_date
123
161
  end
124
162
  end
125
163
  end
@@ -1,12 +1,16 @@
1
1
  # Source: https://gist.github.com/mattetti/1015948
2
2
 
3
3
  module LoanCreator::ExcelFormulas
4
+ # Returns the interest payment
5
+ # for a given period for an investment based on periodic, constant payments and a constant interest rate.
4
6
  def ipmt(rate, per, nper, pv, fv=0, type=0)
5
7
  p = _pmt(rate, nper, pv, fv, 0);
6
8
  ip = -(pv * _pow1p(rate, per - 1) * rate + p * _pow1pm1(rate, per - 1))
7
9
  (type == 0) ? ip : ip / (1 + rate)
8
10
  end
9
11
 
12
+ # Returns the payment on the principal
13
+ # for a given period for an investment based on periodic, constant payments and a constant interest rate.
10
14
  def ppmt(rate, per, nper, pv, fv=0, type=0)
11
15
  p = _pmt(rate, nper, pv, fv, type)
12
16
  ip = ipmt(rate, per, nper, pv, fv, type)
@@ -3,9 +3,7 @@ module LoanCreator
3
3
  # InFine is the same as a Linear loan with (duration - 1) deferred periods.
4
4
  # Thus we're generating a Linear loan instead of rewriting already existing code.
5
5
  def lender_timetable
6
- raise ArgumentError.new(:deferred_in_periods) unless deferred_in_periods == 0
7
- options = { deferred_in_periods: duration_in_periods - 1 }
8
- options = REQUIRED_ATTRIBUTES.each_with_object(options) { |k,h| h[k] = send(k) }
6
+ options = @options.merge(deferred_in_periods: duration_in_periods - 1)
9
7
  LoanCreator::Linear.new(options).lender_timetable
10
8
  end
11
9
  end
@@ -5,10 +5,16 @@ module LoanCreator
5
5
  timetable = new_timetable
6
6
  reset_current_term
7
7
  @crd_end_of_period = amount
8
+
9
+ if term_zero?
10
+ compute_term_zero
11
+ timetable << current_term
12
+ end
13
+
8
14
  duration_in_periods.times do |idx|
9
15
  @last_period = last_period?(idx)
10
16
  @deferred_period = idx < deferred_in_periods
11
- compute_current_term
17
+ compute_current_term(idx)
12
18
  timetable << current_term
13
19
  end
14
20
  timetable
@@ -20,7 +26,7 @@ module LoanCreator
20
26
  idx == (duration_in_periods - 1)
21
27
  end
22
28
 
23
- def compute_current_term
29
+ def compute_current_term(idx)
24
30
  # Reminder: CRD beginning of period = CRD end of period **of previous period**
25
31
  @crd_beginning_of_period = @crd_end_of_period
26
32
  @period_theoric_interests = @crd_beginning_of_period * periodic_interests_rate
@@ -42,6 +48,8 @@ module LoanCreator
42
48
  @total_paid_interests_end_of_period += @period_interests
43
49
  @period_amount_to_pay = @period_interests + @period_capital
44
50
  @crd_end_of_period -= @period_capital
51
+ @due_on = nil
52
+ @index = idx + 1
45
53
  end
46
54
 
47
55
  def period_capital
@@ -6,6 +6,12 @@ module LoanCreator
6
6
  timetable = new_timetable
7
7
  reset_current_term
8
8
  @crd_end_of_period = amount
9
+
10
+ if term_zero?
11
+ compute_term_zero
12
+ timetable << current_term
13
+ end
14
+
9
15
  duration_in_periods.times do |idx|
10
16
  @last_period = last_period?(idx)
11
17
  @deferred_period = idx < deferred_in_periods
@@ -42,6 +48,8 @@ module LoanCreator
42
48
  @total_paid_interests_end_of_period += @period_interests
43
49
  @period_amount_to_pay = @period_interests + @period_capital
44
50
  @crd_end_of_period -= @period_capital
51
+ @due_on = nil
52
+ @index = idx + 1
45
53
  end
46
54
 
47
55
  def period_theoric_interests(idx)
@@ -36,7 +36,7 @@ module LoanCreator
36
36
  :period_amount_to_pay
37
37
  ].freeze
38
38
 
39
- ATTRIBUTES = [
39
+ OPTIONAL_ARGUMENTS = [
40
40
  # Term number (starts at 1)
41
41
  # This value is to be set by Timetable
42
42
  :index,
@@ -44,15 +44,15 @@ module LoanCreator
44
44
  # Term date
45
45
  # This value is to be set by Timetable
46
46
  :due_on,
47
+ ]
47
48
 
48
- # These values are to be specified during Term's initialization
49
- *ARGUMENTS
50
- ].freeze
49
+ ATTRIBUTES = (ARGUMENTS + OPTIONAL_ARGUMENTS).freeze
51
50
 
52
51
  attr_accessor *ATTRIBUTES
53
52
 
54
53
  def initialize(**options)
55
54
  ARGUMENTS.each { |k| instance_variable_set(:"@#{k}", options.fetch(k)) }
55
+ OPTIONAL_ARGUMENTS.each { |k| instance_variable_set(:"@#{k}", options.fetch(k, nil)) }
56
56
  end
57
57
 
58
58
  def to_csv
@@ -3,39 +3,34 @@ module LoanCreator
3
3
  class Timetable
4
4
  # Used to calculate next term's date (see ActiveSupport#advance)
5
5
  PERIODS = {
6
- month: { months: 1 },
7
- quarter: { months: 3 },
8
- semester: { months: 6 },
9
- year: { years: 1 }
6
+ month: {months: 1},
7
+ quarter: {months: 3},
8
+ semester: {months: 6},
9
+ year: {years: 1}
10
10
  }
11
11
 
12
- attr_reader :terms, :starts_on, :period
12
+ attr_reader :terms, :starts_on, :period #, :interests_start_date
13
13
 
14
- def initialize(starts_on:, period:)
15
- @terms = []
16
- @starts_on = (Date === starts_on ? starts_on : Date.parse(starts_on))
14
+ def initialize(starts_on:, period:, interests_start_date: nil)
17
15
  raise ArgumentError.new(:period) unless PERIODS.keys.include?(period)
18
- @period = period
16
+
17
+ @terms = []
18
+ @starts_on = (starts_on.is_a?(Date) ? starts_on : Date.parse(starts_on))
19
+ @period = period
20
+
21
+ if interests_start_date
22
+ @interests_start_date = (interests_start_date.is_a?(Date) ? interests_start_date : Date.parse(interests_start_date))
23
+ end
19
24
  end
20
25
 
21
26
  def <<(term)
22
- raise ArgumentError.new('LoanCreator::Term expected') unless LoanCreator::Term === term
23
- term.index = autoincrement_index
24
- term.due_on = autoincrement_date
27
+ raise ArgumentError.new('LoanCreator::Term expected') unless term.is_a?(LoanCreator::Term)
28
+ term.index ||= autoincrement_index
29
+ term.due_on ||= date_for(term.index)
25
30
  @terms << term
26
31
  self
27
32
  end
28
33
 
29
- def reset_indexes_and_due_on_dates
30
- @autoincrement_index = 0
31
- @autoincrement_date = @starts_on
32
- @terms.each do |term|
33
- term[:index] = autoincrement_index
34
- term[:due_on] = autoincrement_date
35
- end
36
- self
37
- end
38
-
39
34
  def to_csv(header: true)
40
35
  output = []
41
36
  output << terms.first.to_h.keys.join(',') if header
@@ -43,20 +38,33 @@ module LoanCreator
43
38
  output
44
39
  end
45
40
 
41
+ def term(index)
42
+ @terms.find { |term| term.index == index }
43
+ end
44
+
46
45
  private
47
46
 
48
- # First term index of a timetable term is 1
49
47
  def autoincrement_index
50
- @autoincrement_index ||= 0
51
- @autoincrement_index += 1
48
+ @current_index = @current_index.nil? ? 1 : @current_index + 1
49
+ end
50
+
51
+ def date_for(index)
52
+ @_dates ||= Hash.new do |dates, index|
53
+ dates[index] =
54
+ if index < 1
55
+ dates[index + 1].advance(PERIODS.fetch(period).transform_values {|n| -n})
56
+ elsif index == 1
57
+ starts_on
58
+ else
59
+ dates[index - 1].advance(PERIODS.fetch(period))
60
+ end
61
+ end
62
+
63
+ @_dates[index]
52
64
  end
53
65
 
54
- # First term due_on date of timetable term is the starts_on given date
55
- def autoincrement_date
56
- @autoincrement_date ||= @starts_on
57
- date = @autoincrement_date
58
- @autoincrement_date = @autoincrement_date.advance(PERIODS.fetch(@period))
59
- date
66
+ def reset_dates
67
+ @_dates = nil
60
68
  end
61
69
  end
62
70
  end
@@ -1,3 +1,3 @@
1
1
  module LoanCreator
2
- VERSION = '0.2.3'.freeze
2
+ VERSION = '0.5.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loan_creator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thibaulth
@@ -126,6 +126,7 @@ files:
126
126
  - ".ruby-gemset"
127
127
  - ".ruby-version"
128
128
  - ".travis.yml"
129
+ - CapSens_Loan.xlsx
129
130
  - Gemfile
130
131
  - LICENSE.txt
131
132
  - README.md