ec-abacus 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7a06d9174b4665f2d0f6c72826755accf882558c3003b6831742fd08bb807c0f
4
+ data.tar.gz: 3c8594c380161da7cbbddbc8cacc50cf8b57438563e93f83a89270902bf2dfd9
5
+ SHA512:
6
+ metadata.gz: 2068130a9795faf9805e3b422f4813324e4c21b863c918650b2cf52b36f6a7a92bf7e2a77fd6fcb21e53284f7c8e9ae6c590800f130fd61ee4e8531a8afd5baa
7
+ data.tar.gz: 12305b271c60c0f89a6b7448306486041600e97e7dece333573221b3deb87c636ba91c54a52a519bfc4b43c78618c4cd04c30158132f344b60530a2b813bb400
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "ec-abacus" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["gxssd624@duck.com"](mailto:"gxssd624@duck.com").
data/Guardfile ADDED
@@ -0,0 +1,70 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features) \
6
+ # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
+
8
+ ## Note: if you are using the `directories` clause above and you are not
9
+ ## watching the project directory ('.'), then you will want to move
10
+ ## the Guardfile to a watched dir and symlink it back, e.g.
11
+ #
12
+ # $ mkdir config
13
+ # $ mv Guardfile config/
14
+ # $ ln -s config/Guardfile .
15
+ #
16
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
+
18
+ # Note: The cmd option is now required due to the increasing number of ways
19
+ # rspec may be run, below are examples of the most common uses.
20
+ # * bundler: 'bundle exec rspec'
21
+ # * bundler binstubs: 'bin/rspec'
22
+ # * spring: 'bin/rspec' (This will use spring if running and you have
23
+ # installed the spring binstubs per the docs)
24
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
25
+ # * 'just' rspec: 'rspec'
26
+
27
+ guard :rspec, cmd: "bundle exec rspec" do
28
+ require "guard/rspec/dsl"
29
+ dsl = Guard::RSpec::Dsl.new(self)
30
+
31
+ # Feel free to open issues for suggestions and improvements
32
+
33
+ # RSpec files
34
+ rspec = dsl.rspec
35
+ watch(rspec.spec_helper) { rspec.spec_dir }
36
+ watch(rspec.spec_support) { rspec.spec_dir }
37
+ watch(rspec.spec_files)
38
+
39
+ # Ruby files
40
+ ruby = dsl.ruby
41
+ dsl.watch_spec_files_for(ruby.lib_files)
42
+
43
+ # Rails files
44
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
45
+ dsl.watch_spec_files_for(rails.app_files)
46
+ dsl.watch_spec_files_for(rails.views)
47
+
48
+ watch(rails.controllers) do |m|
49
+ [
50
+ rspec.spec.call("routing/#{m[1]}_routing"),
51
+ rspec.spec.call("controllers/#{m[1]}_controller"),
52
+ rspec.spec.call("acceptance/#{m[1]}")
53
+ ]
54
+ end
55
+
56
+ # Rails config changes
57
+ watch(rails.spec_helper) { rspec.spec_dir }
58
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
59
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
60
+
61
+ # Capybara features specs
62
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
63
+ watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
64
+
65
+ # Turnip features and steps
66
+ watch(%r{^spec/acceptance/(.+)\.feature$})
67
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
68
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
69
+ end
70
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Eclectic Code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Ec::Abacus
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ec/abacus`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/binnablus/ec-abacus. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/binnablus/ec-abacus/blob/master/CODE_OF_CONDUCT.md).
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
40
+
41
+ ## Code of Conduct
42
+
43
+ Everyone interacting in the Ec::Abacus project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/binnablus/ec-abacus/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Ec
6
+ module Abacus
7
+ class InterestCalculator
8
+
9
+ def initialize(principal:, annual_rate:, start_date:, end_date:, rule:)
10
+ @principal = principal
11
+ @annual_rate = annual_rate # e.g., 0.05 for 5%
12
+ @start_date = Date.parse(start_date)
13
+ @end_date = Date.parse(end_date)
14
+ @rule = rule
15
+ end
16
+
17
+ def calculate_interest
18
+ daily_rate = @annual_rate / 360.0
19
+ @principal * daily_rate * days_count
20
+ end
21
+
22
+ private
23
+ attr_accessor :principal, :annual_rate, :start_date, :end_date, :rule
24
+
25
+ def days_count
26
+ case rule
27
+ when :actual_actual, :actual_360, :actual_365, :fixed_schedule
28
+ # Actual number of days between dates
29
+ (end_date - start_date).to_i
30
+ when :normalized_30_360, :normalized_30_365
31
+ # NASD/ISDA standard 30/360 formulation
32
+ d1 = start_date.day
33
+ m1 = start_date.month
34
+ y1 = start_date.year
35
+
36
+ d2 = end_date.day
37
+ m2 = end_date.month
38
+ y2 = end_date.year
39
+
40
+ # Apply 30/360 adjustment rules
41
+ d1 = 30 if d1 == 31
42
+ d2 = 30 if d2 == 31 && d1 >= 30
43
+
44
+ (360 * (y2 - y1)) + (30 * (m2 - m1)) + (d2 - d1)
45
+ else
46
+ raise ArgumentError, "Unknown day count rule: #{@rule}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # frozen_string_literal: true
53
+
54
+ require "date"
55
+
56
+ # InterestCalculator
57
+ #
58
+ # Calculates interest using the major day count conventions and
59
+ # interest methods used in finance.
60
+ #
61
+ # Day count conventions supported (pass as `convention:`):
62
+ #
63
+ # :thirty_360_us - 30/360 US (Bond Basis). Months are 30 days,
64
+ # year is 360 days, with US end-of-month rules.
65
+ # :thirty_e_360 - 30E/360 (Eurobond Basis). Any 31st becomes the 30th.
66
+ # :thirty_e_360_isda - 30E/360 ISDA. Last day of any month (incl. February)
67
+ # is treated as the 30th, except a maturity date in February.
68
+ # :actual_360 - Actual/360. Actual days elapsed over a 360-day year.
69
+ # :actual_365 - Actual/365 Fixed. Actual days over 365, even in leap years.
70
+ # :actual_actual - Actual/Actual ISDA. Actual days over the actual year
71
+ # length, split across calendar years.
72
+ #
73
+ # Interest methods:
74
+ #
75
+ # .simple - P * r * t
76
+ # .compound - P * ((1 + r/n)^(n*t) - 1), n = compounding frequency
77
+ # .continuous - P * (e^(r*t) - 1)
78
+ # .amortization_schedule - level-payment (actuarial) loan schedule
79
+ # .rule_of_78s - precomputed interest allocated by the Rule of 78s
80
+ #
81
+ # Usage:
82
+ #
83
+ # calc = InterestCalculator.new(
84
+ # principal: 1_000_000,
85
+ # annual_rate: 0.06, # 6% as a decimal
86
+ # start_date: Date.new(2026, 1, 1),
87
+ # end_date: Date.new(2026, 2, 1),
88
+ # convention: :thirty_360_us
89
+ # )
90
+ # calc.simple #=> 5000.0
91
+ # calc.year_fraction #=> 0.08333...
92
+ #
93
+ class InterestCalculator
94
+ CONVENTIONS = %i[
95
+ thirty_360_us
96
+ thirty_e_360
97
+ thirty_e_360_isda
98
+ actual_360
99
+ actual_365
100
+ actual_actual
101
+ ].freeze
102
+
103
+ attr_reader :principal, :annual_rate, :start_date, :end_date, :convention
104
+
105
+ # maturity_date is only relevant for :thirty_e_360_isda, where the period
106
+ # end date is NOT adjusted if it is the maturity date and falls in February.
107
+ def initialize(principal:, annual_rate:, start_date:, end_date:,
108
+ convention: :actual_365, maturity_date: nil)
109
+ unless CONVENTIONS.include?(convention)
110
+ raise ArgumentError,
111
+ "Unknown convention #{convention.inspect}. Valid: #{CONVENTIONS.join(', ')}"
112
+ end
113
+ raise ArgumentError, "end_date must be on or after start_date" if end_date < start_date
114
+ raise ArgumentError, "annual_rate must be non-negative" if annual_rate.negative?
115
+
116
+ @principal = principal.to_f
117
+ @annual_rate = annual_rate.to_f
118
+ @start_date = start_date
119
+ @end_date = end_date
120
+ @convention = convention
121
+ @maturity_date = maturity_date
122
+ end
123
+
124
+ # ---------------------------------------------------------------
125
+ # Day count / year fraction
126
+ # ---------------------------------------------------------------
127
+
128
+ # Fraction of a year between start_date and end_date under the
129
+ # chosen convention. This is the "t" used in all interest formulas.
130
+ def year_fraction
131
+ case convention
132
+ when :thirty_360_us then thirty_360_us_fraction
133
+ when :thirty_e_360 then thirty_e_360_fraction
134
+ when :thirty_e_360_isda then thirty_e_360_isda_fraction
135
+ when :actual_360 then actual_days / 360.0
136
+ when :actual_365 then actual_days / 365.0
137
+ when :actual_actual then actual_actual_fraction
138
+ end
139
+ end
140
+
141
+ # Number of accrual days the convention counts (numerator of the fraction).
142
+ def day_count
143
+ case convention
144
+ when :thirty_360_us then (thirty_360_us_fraction * 360).round
145
+ when :thirty_e_360 then (thirty_e_360_fraction * 360).round
146
+ when :thirty_e_360_isda then (thirty_e_360_isda_fraction * 360).round
147
+ else actual_days
148
+ end
149
+ end
150
+
151
+ def actual_days
152
+ (end_date - start_date).to_i
153
+ end
154
+
155
+ # ---------------------------------------------------------------
156
+ # Interest methods
157
+ # ---------------------------------------------------------------
158
+
159
+ # Simple interest: P * r * t
160
+ def simple
161
+ principal * annual_rate * year_fraction
162
+ end
163
+
164
+ # Compound interest earned (not the balance): P * ((1 + r/n)^(n*t) - 1)
165
+ # frequency: compounding periods per year (12 = monthly, 4 = quarterly,
166
+ # 1 = annually, 365 = daily).
167
+ def compound(frequency: 12)
168
+ raise ArgumentError, "frequency must be positive" unless frequency.positive?
169
+
170
+ principal * ((1 + annual_rate / frequency)**(frequency * year_fraction) - 1)
171
+ end
172
+
173
+ # Continuously compounded interest earned: P * (e^(r*t) - 1)
174
+ def continuous
175
+ principal * (Math.exp(annual_rate * year_fraction) - 1)
176
+ end
177
+
178
+ # Future value under compound interest (principal + interest).
179
+ def future_value(frequency: 12)
180
+ principal + compound(frequency: frequency)
181
+ end
182
+
183
+ # ---------------------------------------------------------------
184
+ # Loan-style methods
185
+ # ---------------------------------------------------------------
186
+
187
+ # Level monthly payment for a fully amortizing loan (actuarial method).
188
+ def amortized_payment(months:)
189
+ raise ArgumentError, "months must be positive" unless months.positive?
190
+
191
+ monthly_rate = annual_rate / 12.0
192
+ return principal / months if monthly_rate.zero?
193
+
194
+ principal * monthly_rate / (1 - (1 + monthly_rate)**-months)
195
+ end
196
+
197
+ # Full amortization schedule. Returns an array of hashes:
198
+ # { period:, payment:, interest:, principal:, balance: }
199
+ def amortization_schedule(months:)
200
+ payment = amortized_payment(months: months)
201
+ monthly_rate = annual_rate / 12.0
202
+ balance = principal
203
+
204
+ (1..months).map do |period|
205
+ interest = balance * monthly_rate
206
+ principal_paid = payment - interest
207
+ # Final payment absorbs rounding drift so the balance lands on zero.
208
+ if period == months
209
+ principal_paid = balance
210
+ payment_this = balance + interest
211
+ else
212
+ payment_this = payment
213
+ end
214
+ balance -= principal_paid
215
+ {
216
+ period: period,
217
+ payment: payment_this.round(2),
218
+ interest: interest.round(2),
219
+ principal: principal_paid.round(2),
220
+ balance: balance.abs < 0.005 ? 0.0 : balance.round(2)
221
+ }
222
+ end
223
+ end
224
+
225
+ # Rule of 78s (sum-of-digits) allocation of precomputed interest.
226
+ #
227
+ # total_interest defaults to simple interest for the term implied by
228
+ # `months` (months/12 of a year), which is how precomputed loans are
229
+ # typically quoted. Returns an array of per-period interest charges,
230
+ # front-loaded: period 1 carries months/sum, period 2 (months-1)/sum, etc.
231
+ def rule_of_78s(months:, total_interest: nil)
232
+ raise ArgumentError, "months must be positive" unless months.positive?
233
+
234
+ total = total_interest || principal * annual_rate * (months / 12.0)
235
+ sum_of_digits = months * (months + 1) / 2
236
+
237
+ (1..months).map do |period|
238
+ weight = months - period + 1
239
+ { period: period, interest: (total * weight / sum_of_digits).round(2) }
240
+ end
241
+ end
242
+
243
+ # ---------------------------------------------------------------
244
+ # Convenience: compare every convention for the same period
245
+ # ---------------------------------------------------------------
246
+
247
+ def self.compare(principal:, annual_rate:, start_date:, end_date:)
248
+ CONVENTIONS.to_h do |conv|
249
+ calc = new(principal: principal, annual_rate: annual_rate,
250
+ start_date: start_date, end_date: end_date, convention: conv)
251
+ [conv, { days: calc.day_count, year_fraction: calc.year_fraction, simple_interest: calc.simple.round(2) }]
252
+ end
253
+ end
254
+
255
+ private
256
+
257
+ # --- 30/360 US (Bond Basis) --------------------------------------
258
+ # 1. If d1 is 31, set d1 to 30.
259
+ # 2. If d2 is 31 and d1 is 30 (after step 1), set d2 to 30.
260
+ def thirty_360_us_fraction
261
+ d1 = start_date.day
262
+ d2 = end_date.day
263
+ d1 = 30 if d1 == 31
264
+ d2 = 30 if d2 == 31 && d1 == 30
265
+ thirty_360(start_date.year, start_date.month, d1, end_date.year, end_date.month, d2)
266
+ end
267
+
268
+ # --- 30E/360 (Eurobond Basis) ------------------------------------
269
+ # Any date on the 31st becomes the 30th, unconditionally.
270
+ def thirty_e_360_fraction
271
+ d1 = [start_date.day, 30].min
272
+ d2 = [end_date.day, 30].min
273
+ thirty_360(start_date.year, start_date.month, d1, end_date.year, end_date.month, d2)
274
+ end
275
+
276
+ # --- 30E/360 ISDA ------------------------------------------------
277
+ # d1 = 30 if start_date is the last day of its month.
278
+ # d2 = 30 if end_date is the last day of its month, UNLESS end_date is
279
+ # the maturity date and falls in February.
280
+ def thirty_e_360_isda_fraction
281
+ d1 = last_day_of_month?(start_date) ? 30 : start_date.day
282
+ d2 = end_date.day
283
+ if last_day_of_month?(end_date) &&
284
+ !(end_date.month == 2 && @maturity_date && end_date == @maturity_date)
285
+ d2 = 30
286
+ end
287
+ thirty_360(start_date.year, start_date.month, d1, end_date.year, end_date.month, d2)
288
+ end
289
+
290
+ def thirty_360(y1, m1, d1, y2, m2, d2)
291
+ (360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)) / 360.0
292
+ end
293
+
294
+ def last_day_of_month?(date)
295
+ date.day == Date.new(date.year, date.month, -1).day
296
+ end
297
+
298
+ # --- Actual/Actual ISDA ------------------------------------------
299
+ # Days in each calendar year divided by that year's actual length
300
+ # (366 for leap years, 365 otherwise), summed across the period.
301
+ def actual_actual_fraction
302
+ return 0.0 if start_date == end_date
303
+
304
+ (start_date.year..end_date.year).sum do |year|
305
+ year_start = [Date.new(year, 1, 1), start_date].max
306
+ year_end = [Date.new(year + 1, 1, 1), end_date].min
307
+ days = (year_end - year_start).to_i
308
+ next 0.0 if days <= 0
309
+
310
+ days / (Date.gregorian_leap?(year) ? 366.0 : 365.0)
311
+ end
312
+ end
313
+ end
314
+
315
+
316
+
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Ec
6
+ module Abacus
7
+ class SimpleInterest
8
+ def initialize(principal:, annual_rate:, start_date:, end_date:, rule:, period: nil)
9
+ @principal = principal
10
+ @annual_rate = annual_rate # e.g., 0.05 for 5%
11
+ @start_date = Date.parse(start_date)
12
+ @end_date = Date.parse(end_date)
13
+ @rule = rule
14
+ end
15
+
16
+ def call
17
+ principal * annual_rate * year_fraction
18
+ end
19
+
20
+ private
21
+ attr_accessor :principal, :annual_rate, :start_date, :end_date, :rule
22
+
23
+ # Fraction of a year between start_date and end_date under the
24
+ # chosen convention. This is the "t" used in all interest formulas.
25
+ def year_fraction
26
+ case rule
27
+ when :actual_365 then actual_days / 365.0
28
+ when :actual_360 then actual_days / 360.0
29
+ when :actual_actual then actual_actual_fraction
30
+ when :normalized_30_360 then thirty_days / 360.0
31
+ when :normalized_30_365 then thirty_days / 365.0
32
+ when :fixed_fee_no_interest then 0
33
+ end
34
+ end
35
+
36
+ def thirty_days
37
+ day1 = [start_date.day, 30].min
38
+ day2 = end_date.day
39
+
40
+ if day1 == 30
41
+ day2 = [day2, 30].min
42
+ end
43
+
44
+ (
45
+ 360 * (end_date.year - start_date.year) +
46
+ 30 * (end_date.month - start_date.month) +
47
+ (day2 - day1)
48
+ )
49
+ end
50
+
51
+ def actual_days
52
+ (end_date - start_date).to_i
53
+ end
54
+
55
+ # --- Actual/Actual ISDA ------------------------------------------
56
+ # Days in each calendar year divided by that year's actual length
57
+ # (366 for leap years, 365 otherwise), summed across the period.
58
+ def actual_actual_fraction
59
+ if start_date == end_date
60
+ 0.0
61
+ else
62
+ (start_date.year..end_date.year).sum do |year|
63
+ period_start = [Date.new(year, 1, 1), start_date].max
64
+ period_end = [Date.new(year + 1, 1, 1), end_date].min
65
+ days = (period_end - period_start).to_i
66
+ next 0.0 if days <= 0
67
+
68
+ days / (Date.gregorian_leap?(year) ? 366.0 : 365.0)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ec
4
+ module Abacus
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/ec/abacus.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abacus/version"
4
+
5
+ module Ec
6
+ module Abacus
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+
10
+ autoload :InterestCalculator, 'abacus/interest_calculator'
11
+ end
12
+ end
data/sig/ec/abacus.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Ec
2
+ module Abacus
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec-abacus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eclectic Code
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Provide various financial calculations.
13
+ email:
14
+ - gxssd624@duck.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - CODE_OF_CONDUCT.md
20
+ - Guardfile
21
+ - LICENSE.txt
22
+ - README.md
23
+ - Rakefile
24
+ - lib/ec/abacus.rb
25
+ - lib/ec/abacus/interest_calculator.rb
26
+ - lib/ec/abacus/simple_interest.rb
27
+ - lib/ec/abacus/version.rb
28
+ - sig/ec/abacus.rbs
29
+ homepage: https://github.com/binnablus/ec-abacus
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/binnablus/ec-abacus
34
+ source_code_uri: https://github.com/binnablus/ec-abacus
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 3.2.0
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 4.0.10
50
+ specification_version: 4
51
+ summary: Provide various financial calculations.
52
+ test_files: []