loan_creator 0.8.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c39773817a99d44a421aac19ea019f43ba24bba4359f8f61630b480f47662c65
4
- data.tar.gz: b645544fb96db2d5bf60c8b8e811e8b08e54d49d7c5fb62f2af4d54e4619d64d
3
+ metadata.gz: 4dcaafbce29a00ebcdea57133619e566cc210be6ed613003af14e0a199cc126a
4
+ data.tar.gz: 2da6fe223cfc40fd8455a5c1ec0d8ef857bfd48effb22b285f46da7c3800a81b
5
5
  SHA512:
6
- metadata.gz: e24e2f465c85252f8654a62fc252f0f82f75ffd0690b90ee586517769ad7055d24cecd4e7694f73dc6e54f2260969bffff01f65864e161e2cd89e5bb817477ac
7
- data.tar.gz: 7428313e7ca37b6f10b5f20375c007ea1af3d00a1dc7ed95f0d82a56543267a7c37f7604a931f541c2a93ca1bb953d15d774fa7cad064ef08c846c40adfa8ca0
6
+ metadata.gz: 215f9a9b34c5ac6f9eef09f8658bbcd7c322e1964f0d4a0c9f55d3f97c99d1c9fc804c9e69f2c5509085bca9ec6ed848e92561ce456d6348d7b5ea343e38ada6
7
+ data.tar.gz: 98b6cf146fd802777c156266cd9378d8816ef00d4c4ab8aafdc092f4b52b001e2b8862d61a260502803f888f3b7f0f1b34bea5e29ea4a5843f8b01b5d99ce729
data/CHANGELOG.md CHANGED
@@ -1,14 +1,18 @@
1
+ v0.8.1
2
+ -------------------------
3
+ - add `realistic_durations` option
4
+
1
5
  v0.8.0
2
6
  -------------------------
3
7
 
4
- ### FEATURE
8
+ #### Feature
5
9
  - due interests to date are now store into `due_interests_` columns for `LoanCreator::UncapitalizedBullet`
6
10
 
7
- ### BUGFIX
11
+ #### Bugfix
8
12
  - fix `initial_values` not taking `paid_capital` and `paid_interests` into account for `LoanCreator::UncapitalizedBullet`
9
13
  and `LoanCreator::Bullet`
10
14
 
11
- ### BREAKING CHANGES
15
+ #### Breaking changes
12
16
  - rename columns `capitalized_interests_beginning_of_period` and `capitalized_interests_end_of_period` for
13
17
  `due_interests_beginning_of_period` and `due_interests_end_of_period`
14
18
  - rename `capitalized_interests` option in `:inital_values` into `due_interests`
data/README.md CHANGED
@@ -44,9 +44,10 @@ Each instance of one of the previous classes has the following attributes:
44
44
  :annual_interests_rate
45
45
  :starts_at
46
46
  :duration_in_periods
47
- :deferred_in_periods (default to zero)
47
+ :deferred_in_periods (defaults to zero)
48
48
  :interests_start_date (optional)
49
49
  :initial_values (to generate a timetable from a previous term or at a given state)
50
+ :realistic_durations (optional, defaults to false)
50
51
  ```
51
52
 
52
53
  Initial values must be a hash with specific keys, like so:
@@ -102,6 +103,9 @@ There is also a `LoanCreator::Timetable` class dedicated to record the data of t
102
103
 
103
104
  # Amount to pay this term
104
105
  :period_amount_to_pay
106
+
107
+ # Whether or not to use the real number of days in each month
108
+ :realistic_durations
105
109
  ```
106
110
 
107
111
  `#periodic_interests_rate` renders a precise calculation of the loan's periodic interests rate based on two inputs: `#annual_interests_rate` and `#period`.
@@ -176,6 +180,9 @@ additional term with only interests for the time difference.
176
180
  For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
177
181
  first term corresponding to 3 months of interests.
178
182
 
183
+ `realistic_durations`: Optional. A boolean tha specifies whether or not to use the real number of days in each month when calculating the periodic interests rate. Note that leap years are taken into account. Default: `false`.
184
+ The default behaviour is to use the `period` in relation to the number of months in a year (ie: 12)
185
+
179
186
  ## Calculation
180
187
 
181
188
  An excel simulator for standard case can be found [here](CapSens_Loan.xlsx).
@@ -21,8 +21,7 @@ module LoanCreator
21
21
 
22
22
  borrower_timetable = LoanCreator::Timetable.new(
23
23
  starting_index: lenders_timetables.first.starting_index,
24
- starts_on: lenders_timetables.first.starts_on,
25
- period: lenders_timetables.first.period
24
+ loan: lenders_timetables.first.loan
26
25
  )
27
26
 
28
27
  # Borrower timetable is not concerned with computation-related value (delta, etc.),
@@ -7,18 +7,24 @@ module LoanCreator
7
7
  reset_current_term
8
8
  @crd_beginning_of_period = amount
9
9
  @crd_end_of_period = amount
10
- (duration_in_periods - 1).times { |period| compute_term(timetable) }
11
- compute_last_term
12
- timetable << current_term
10
+
11
+ duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
12
+
13
13
  timetable
14
14
  end
15
15
 
16
16
  private
17
17
 
18
- def compute_last_term
18
+ def compute_current_term(idx, timetable)
19
+ @due_on = timetable_term_dates[timetable.next_index]
20
+ last_period?(idx) ? compute_last_term(timetable) : compute_term(timetable)
21
+ current_term
22
+ end
23
+
24
+ def compute_last_term(timetable)
19
25
  @crd_end_of_period = bigd('0')
20
26
  @due_interests_beginning_of_period = @due_interests_end_of_period
21
- @period_interests = @due_interests_end_of_period + compute_capitalized_interests
27
+ @period_interests = @due_interests_end_of_period + compute_capitalized_interests(@due_on, timetable)
22
28
  @due_interests_end_of_period = 0
23
29
  @period_capital = @crd_beginning_of_period
24
30
  @total_paid_capital_end_of_period += @period_capital
@@ -26,14 +32,14 @@ module LoanCreator
26
32
  @period_amount_to_pay = @period_capital + @period_interests
27
33
  end
28
34
 
29
- def compute_capitalized_interests
30
- (amount + @due_interests_beginning_of_period).mult(periodic_interests_rate, BIG_DECIMAL_DIGITS)
35
+ def compute_capitalized_interests(due_date, timetable)
36
+ computed_periodic_interests_rate = periodic_interests_rate(due_date, relative_to_date: timetable_term_dates[timetable.next_index - 1])
37
+ (amount + @due_interests_beginning_of_period).mult(computed_periodic_interests_rate, BIG_DECIMAL_DIGITS)
31
38
  end
32
39
 
33
40
  def compute_term(timetable)
34
41
  @due_interests_beginning_of_period = @due_interests_end_of_period
35
- @due_interests_end_of_period += compute_capitalized_interests
36
- timetable << current_term
42
+ @due_interests_end_of_period += compute_capitalized_interests(@due_on, timetable)
37
43
  end
38
44
  end
39
45
  end
@@ -21,7 +21,8 @@ module LoanCreator
21
21
  # attribute: default_value
22
22
  deferred_in_periods: 0,
23
23
  interests_start_date: nil,
24
- initial_values: {}
24
+ initial_values: {},
25
+ realistic_durations: false
25
26
  }.freeze
26
27
 
27
28
  attr_reader *REQUIRED_ATTRIBUTES
@@ -37,14 +38,26 @@ module LoanCreator
37
38
  validate_initial_values
38
39
  end
39
40
 
40
- def periodic_interests_rate_percentage
41
- @periodic_interests_rate_percentage ||=
42
- annual_interests_rate.div(12 / PERIODS_IN_MONTHS[period], BIG_DECIMAL_DIGITS)
41
+ def periodic_interests_rate(date = nil, relative_to_date: nil)
42
+ if realistic_durations?
43
+ compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date: relative_to_date).div(100, BIG_DECIMAL_DIGITS)
44
+ else
45
+ @periodic_interests_rate ||=
46
+ annual_interests_rate.div(12 / PERIODS_IN_MONTHS[period], BIG_DECIMAL_DIGITS).div(100, BIG_DECIMAL_DIGITS)
47
+ end
43
48
  end
44
49
 
45
- def periodic_interests_rate
46
- @periodic_interests_rate ||=
47
- periodic_interests_rate_percentage.div(100, BIG_DECIMAL_DIGITS)
50
+ def timetable_term_dates
51
+ @_timetable_term_dates ||= Hash.new do |dates, index|
52
+ dates[index] =
53
+ if index < 1
54
+ dates[index + 1].advance(months: -PERIODS_IN_MONTHS.fetch(period))
55
+ elsif index == 1
56
+ starts_on
57
+ else
58
+ starts_on.advance(months: PERIODS_IN_MONTHS.fetch(period) * (index - 1))
59
+ end
60
+ end
48
61
  end
49
62
 
50
63
  def lender_timetable
@@ -152,8 +165,7 @@ module LoanCreator
152
165
 
153
166
  def new_timetable
154
167
  LoanCreator::Timetable.new(
155
- starts_on: starts_on,
156
- period: period,
168
+ loan: self,
157
169
  interests_start_date: interests_start_date,
158
170
  starting_index: @starting_index
159
171
  )
@@ -163,6 +175,10 @@ module LoanCreator
163
175
  @index ? (@starting_index + @index - 1) : nil
164
176
  end
165
177
 
178
+ def last_period?(idx)
179
+ idx == (duration_in_periods - 1)
180
+ end
181
+
166
182
  def compute_term_zero
167
183
  @crd_beginning_of_period = @crd_end_of_period
168
184
  @period_theoric_interests = term_zero_interests
@@ -172,6 +188,7 @@ module LoanCreator
172
188
  @total_paid_interests_end_of_period += @period_interests
173
189
  @period_amount_to_pay = @period_interests
174
190
  @index = 0
191
+ @due_on = timetable_term_dates[0]
175
192
  end
176
193
 
177
194
  def term_zero_interests
@@ -194,5 +211,17 @@ module LoanCreator
194
211
  def term_zero?
195
212
  interests_start_date && interests_start_date < term_zero_date
196
213
  end
214
+
215
+ def compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date:)
216
+ realistic_days = 365
217
+ realistic_days += 1 if date.leap?
218
+ realistic_days_in_period = (date - relative_to_date).to_i
219
+
220
+ annual_interests_rate.div(bigd(realistic_days) / bigd(realistic_days_in_period), BIG_DECIMAL_DIGITS)
221
+ end
222
+
223
+ def realistic_durations?
224
+ !!@realistic_durations
225
+ end
197
226
  end
198
227
  end
@@ -11,26 +11,23 @@ module LoanCreator
11
11
  timetable << current_term
12
12
  end
13
13
 
14
- duration_in_periods.times do |idx|
15
- @last_period = last_period?(idx)
16
- @deferred_period = idx < deferred_in_periods
17
- compute_current_term(idx)
18
- timetable << current_term
19
- end
14
+ duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
20
15
 
21
16
  timetable
22
17
  end
23
18
 
24
19
  private
25
20
 
26
- def last_period?(idx)
27
- idx == (duration_in_periods - 1)
28
- end
21
+ def compute_current_term(idx, timetable)
22
+ @index = idx + 1
23
+ @last_period = last_period?(idx)
24
+ @deferred_period = @index <= deferred_in_periods
25
+ @due_on = timetable_term_dates[timetable.next_index]
26
+ computed_periodic_interests_rate = periodic_interests_rate(@due_on, relative_to_date: timetable_term_dates[timetable.next_index - 1])
29
27
 
30
- def compute_current_term(idx)
31
28
  # Reminder: CRD beginning of period = CRD end of period **of previous period**
32
29
  @crd_beginning_of_period = @crd_end_of_period
33
- @period_theoric_interests = @crd_beginning_of_period * periodic_interests_rate
30
+ @period_theoric_interests = @crd_beginning_of_period * computed_periodic_interests_rate
34
31
  @delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
35
32
  @accrued_delta_interests += @delta_interests
36
33
  @amount_to_add = bigd(
@@ -49,8 +46,8 @@ module LoanCreator
49
46
  @total_paid_interests_end_of_period += @period_interests
50
47
  @period_amount_to_pay = @period_interests + @period_capital
51
48
  @crd_end_of_period -= @period_capital
52
- @due_on = nil
53
- @index = idx + 1
49
+
50
+ current_term
54
51
  end
55
52
 
56
53
  def period_capital
@@ -12,24 +12,22 @@ module LoanCreator
12
12
  timetable << current_term
13
13
  end
14
14
 
15
- duration_in_periods.times do |idx|
16
- @last_period = last_period?(idx)
17
- @deferred_period = idx < deferred_in_periods
18
- compute_current_term(idx)
19
- timetable << current_term
20
- end
15
+ duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
16
+
21
17
  timetable
22
18
  end
23
19
 
24
20
  private
25
21
 
26
- def last_period?(idx)
27
- idx == (duration_in_periods - 1)
28
- end
22
+ def compute_current_term(idx, timetable)
23
+ @index = idx + 1
24
+ @last_period = last_period?(idx)
25
+ @deferred_period = @index <= deferred_in_periods
26
+ @due_on = timetable_term_dates[timetable.next_index]
27
+ computed_periodic_interests_rate = periodic_interests_rate(@due_on, relative_to_date: timetable_term_dates[timetable.next_index - 1])
29
28
 
30
- def compute_current_term(idx)
31
29
  @crd_beginning_of_period = @crd_end_of_period
32
- @period_theoric_interests = period_theoric_interests(idx)
30
+ @period_theoric_interests = period_theoric_interests(@index, computed_periodic_interests_rate)
33
31
  @delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
34
32
  @accrued_delta_interests += @delta_interests
35
33
  @amount_to_add = bigd(
@@ -43,37 +41,37 @@ module LoanCreator
43
41
  )
44
42
  @accrued_delta_interests -= @amount_to_add
45
43
  @period_interests = @period_theoric_interests.round(2) + @amount_to_add
46
- @period_capital = period_capital(idx)
44
+ @period_capital = period_capital(@index, computed_periodic_interests_rate)
47
45
  @total_paid_capital_end_of_period += @period_capital
48
46
  @total_paid_interests_end_of_period += @period_interests
49
47
  @period_amount_to_pay = @period_interests + @period_capital
50
48
  @crd_end_of_period -= @period_capital
51
- @due_on = nil
52
- @index = idx + 1
49
+
50
+ current_term
53
51
  end
54
52
 
55
- def period_theoric_interests(idx)
53
+ def period_theoric_interests(idx, computed_periodic_interests_rate)
56
54
  if @deferred_period
57
- @crd_beginning_of_period * periodic_interests_rate
55
+ @crd_beginning_of_period * computed_periodic_interests_rate
58
56
  else
59
57
  -ipmt(
60
- periodic_interests_rate,
61
- (idx + 1) - deferred_in_periods,
58
+ computed_periodic_interests_rate,
59
+ idx - deferred_in_periods,
62
60
  duration_in_periods - deferred_in_periods,
63
61
  amount
64
62
  )
65
63
  end
66
64
  end
67
65
 
68
- def period_capital(idx)
66
+ def period_capital(idx, computed_periodic_interests_rate)
69
67
  if @last_period
70
68
  @crd_beginning_of_period
71
69
  elsif @deferred_period
72
70
  bigd(0)
73
71
  else
74
72
  -ppmt(
75
- periodic_interests_rate,
76
- (idx + 1) - deferred_in_periods,
73
+ computed_periodic_interests_rate,
74
+ idx - deferred_in_periods,
77
75
  duration_in_periods - deferred_in_periods,
78
76
  amount
79
77
  ).round(2)
@@ -1,22 +1,13 @@
1
1
  # coding: utf-8
2
2
  module LoanCreator
3
3
  class Timetable
4
- # Used to calculate next term's date (see ActiveSupport#advance)
5
- PERIODS = {
6
- month: {months: 1},
7
- quarter: {months: 3},
8
- semester: {months: 6},
9
- year: {years: 1}
10
- }
4
+ attr_reader :loan, :terms, :starting_index #, :interests_start_date
11
5
 
12
- attr_reader :terms, :starts_on, :period, :starting_index #, :interests_start_date
13
-
14
- def initialize(starts_on:, period:, interests_start_date: nil, starting_index: 1)
15
- raise ArgumentError.new(:period) unless PERIODS.keys.include?(period)
6
+ delegate :starts_on, :period, to: :loan
16
7
 
8
+ def initialize(loan:, interests_start_date: nil, starting_index: 1)
17
9
  @terms = []
18
- @starts_on = (starts_on.is_a?(Date) ? starts_on : Date.parse(starts_on))
19
- @period = period
10
+ @loan = loan
20
11
  @starting_index = starting_index
21
12
 
22
13
  if interests_start_date
@@ -26,8 +17,11 @@ module LoanCreator
26
17
 
27
18
  def <<(term)
28
19
  raise ArgumentError.new('LoanCreator::Term expected') unless term.is_a?(LoanCreator::Term)
29
- term.index ||= autoincrement_index
30
- term.due_on ||= date_for(term.index)
20
+
21
+ @current_index = term.index || next_index
22
+
23
+ term.index = @current_index
24
+ term.due_on ||= loan.timetable_term_dates[term.index]
31
25
  @terms << term
32
26
  self
33
27
  end
@@ -43,29 +37,8 @@ module LoanCreator
43
37
  @terms.find { |term| term.index == index }
44
38
  end
45
39
 
46
- private
47
-
48
- def autoincrement_index
49
- @current_index = (@current_index.nil? ? @starting_index : @current_index + 1)
50
- end
51
-
52
- def date_for(index)
53
- @_dates ||= Hash.new do |dates, index|
54
- dates[index] =
55
- if index < 1
56
- dates[index + 1].advance(PERIODS.fetch(period).transform_values {|n| -n})
57
- elsif index == 1
58
- starts_on
59
- else
60
- dates[index - 1].advance(PERIODS.fetch(period))
61
- end
62
- end
63
-
64
- @_dates[index]
65
- end
66
-
67
- def reset_dates
68
- @_dates = nil
40
+ def next_index
41
+ @current_index.nil? ? @starting_index : @current_index + 1
69
42
  end
70
43
  end
71
44
  end
@@ -7,18 +7,24 @@ module LoanCreator
7
7
  reset_current_term
8
8
  @crd_beginning_of_period = amount
9
9
  @crd_end_of_period = amount
10
- (duration_in_periods - 1).times { |period| compute_term(timetable) }
11
- compute_last_term
12
- timetable << current_term
10
+
11
+ duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
12
+
13
13
  timetable
14
14
  end
15
15
 
16
16
  private
17
17
 
18
- def compute_last_term
18
+ def compute_current_term(idx, timetable)
19
+ @due_on = timetable_term_dates[timetable.next_index]
20
+ last_period?(idx) ? compute_last_term(timetable) : compute_term(timetable)
21
+ current_term
22
+ end
23
+
24
+ def compute_last_term(timetable)
19
25
  @crd_end_of_period = bigd('0')
20
26
  @due_interests_beginning_of_period = @due_interests_end_of_period
21
- @period_interests = @due_interests_end_of_period + compute_interests
27
+ @period_interests = @due_interests_end_of_period + compute_interests(@due_on, timetable)
22
28
  @due_interests_end_of_period = 0
23
29
  @period_capital = @crd_beginning_of_period
24
30
  @total_paid_capital_end_of_period += @period_capital
@@ -26,14 +32,14 @@ module LoanCreator
26
32
  @period_amount_to_pay = @period_capital + @period_interests
27
33
  end
28
34
 
29
- def compute_interests
30
- amount.mult(bigd(periodic_interests_rate), BIG_DECIMAL_DIGITS)
35
+ def compute_interests(due_date, timetable)
36
+ computed_periodic_interests_rate = periodic_interests_rate(due_date, relative_to_date: timetable_term_dates[timetable.next_index - 1])
37
+ amount.mult(bigd(computed_periodic_interests_rate), BIG_DECIMAL_DIGITS)
31
38
  end
32
39
 
33
40
  def compute_term(timetable)
34
41
  @due_interests_beginning_of_period = @due_interests_end_of_period
35
- @due_interests_end_of_period += compute_interests
36
- timetable << current_term
42
+ @due_interests_end_of_period += compute_interests(@due_on, timetable)
37
43
  end
38
44
  end
39
45
  end
@@ -1,3 +1,3 @@
1
1
  module LoanCreator
2
- VERSION = '0.8.0'.freeze
2
+ VERSION = '0.8.1'.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.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - thibaulth
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: exe
14
14
  cert_chain: []
15
- date: 2021-02-23 00:00:00.000000000 Z
15
+ date: 2021-04-02 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: bundler