fat_period 2.1.0 → 3.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 +4 -4
- data/.envrc +0 -1
- data/.github/workflows/ruby.yml +57 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.org +43 -0
- data/Gemfile +13 -11
- data/README.md +1291 -0
- data/README.org +1229 -19
- data/Rakefile +25 -5
- data/bin/console +1 -0
- data/fat_period.gemspec +4 -3
- data/lib/fat_period/date.rb +4 -6
- data/lib/fat_period/period.rb +248 -33
- data/lib/fat_period/version.rb +3 -1
- data/lib/fat_period.rb +25 -5
- metadata +23 -11
- data/.github/workflows/gem-push.yml +0 -48
- data/.travis.yml +0 -6
data/Rakefile
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
require "rspec/core/rake_task"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
require '
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
5
|
|
|
6
|
-
RuboCop::RakeTask.new
|
|
7
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
require 'gem_docs'
|
|
9
|
+
GemDocs.install
|
|
10
|
+
|
|
11
|
+
########################################################################
|
|
12
|
+
# Rubocop tasks
|
|
13
|
+
########################################################################
|
|
14
|
+
# Option A (recommended): Keep using Bundler and run rubocop via `bundle exec`.
|
|
15
|
+
# This wrapper task ensures the rubocop run uses the gems from your Gemfile,
|
|
16
|
+
# even when you invoke `rake rubocop` (no need to remember `bundle exec rake`).
|
|
17
|
+
#
|
|
18
|
+
# You can pass extra RuboCop CLI flags with the RUBOCOP_OPTS environment variable:
|
|
19
|
+
# RUBOCOP_OPTS="--format simple" rake rubocop
|
|
20
|
+
|
|
21
|
+
desc 'Run rubocop under `bundle exec`'
|
|
22
|
+
task :rubocop do
|
|
23
|
+
opts = (ENV['RUBOCOP_OPTS'] || '').split
|
|
24
|
+
Bundler.with_unbundled_env do
|
|
25
|
+
sh 'bundle', 'exec', 'rubocop', *opts
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
task default: %i[spec rubocop]
|
data/bin/console
CHANGED
data/fat_period.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
lib = File.expand_path('
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
require 'fat_period/version'
|
|
6
6
|
|
|
@@ -21,5 +21,6 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
# spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
22
22
|
spec.require_paths = ['lib']
|
|
23
23
|
|
|
24
|
-
spec.add_dependency 'fat_core', '>=
|
|
24
|
+
spec.add_dependency 'fat_core', '>= 7.0'
|
|
25
|
+
spec.add_dependency 'fat_date'
|
|
25
26
|
end
|
data/lib/fat_period/date.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module FatPeriod
|
|
2
4
|
# An extension of Date for methods useful with respect to FatPeriod::Periods.
|
|
3
5
|
module Date
|
|
@@ -21,9 +23,5 @@ module FatPeriod
|
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
include FatPeriod::Date
|
|
27
|
-
# @!parse include FatPeriod::Date
|
|
28
|
-
# @!parse extend FatPeriod::Date::ClassMethods
|
|
29
|
-
end
|
|
26
|
+
# Add the period helpers to Ruby's Date class.
|
|
27
|
+
Date.include(FatPeriod::Date)
|
data/lib/fat_period/period.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fat_date'
|
|
4
|
+
require 'bigdecimal'
|
|
3
5
|
|
|
4
6
|
# The Period class represents a range of Dates and supports a variety of
|
|
5
7
|
# operations on those ranges.
|
|
@@ -41,7 +43,7 @@ class Period
|
|
|
41
43
|
# @group Parsing
|
|
42
44
|
#
|
|
43
45
|
# Return a period based on two date specs passed as strings (see
|
|
44
|
-
# `FatCore::Date.
|
|
46
|
+
# `FatCore::Date.spec`), a 'from' and a 'to' spec. The returned period
|
|
45
47
|
# begins on the first day of the period given as the `from` spec and ends on
|
|
46
48
|
# the last day given as the `to` spec. If the to spec is not given or is nil,
|
|
47
49
|
# the from spec is used for both the from- and to-spec.
|
|
@@ -52,15 +54,15 @@ class Period
|
|
|
52
54
|
# # Assuming this executes in December, 2014
|
|
53
55
|
# Period.parse('last_month', 'this_month').inspect #=> Period('2014-11-01..2014-12-31')
|
|
54
56
|
#
|
|
55
|
-
# @param from [String] spec ala FatCore::Date.
|
|
56
|
-
# @param to [String] spec ala FatCore::Date.
|
|
57
|
+
# @param from [String] spec ala FatCore::Date.spec
|
|
58
|
+
# @param to [String] spec ala FatCore::Date.spec
|
|
57
59
|
# @return [Period] from beginning of `from` to end of `to`
|
|
58
60
|
def self.parse(from, to = nil)
|
|
59
61
|
raise ArgumentError, 'Period.parse missing argument' unless from
|
|
60
62
|
|
|
61
63
|
to ||= from
|
|
62
|
-
first = Date.
|
|
63
|
-
second = Date.
|
|
64
|
+
first = Date.spec(from, :from)
|
|
65
|
+
second = Date.spec(to, :to)
|
|
64
66
|
Period.new(first, second) if first && second
|
|
65
67
|
end
|
|
66
68
|
|
|
@@ -75,10 +77,11 @@ class Period
|
|
|
75
77
|
# @param prd [String|Period] or any candidate for conversion to Period
|
|
76
78
|
# @return Period correspondign to prd parameter
|
|
77
79
|
def self.ensure(prd)
|
|
78
|
-
prd.to_period if prd.respond_to?(:to_period)
|
|
80
|
+
return prd.to_period if prd.respond_to?(:to_period)
|
|
81
|
+
|
|
79
82
|
case prd
|
|
80
83
|
when String
|
|
81
|
-
if prd.match(/from|to/i)
|
|
84
|
+
if prd.match?(/from|to/i)
|
|
82
85
|
Period.parse_phrase(prd).first
|
|
83
86
|
else
|
|
84
87
|
Period.parse(prd)
|
|
@@ -110,13 +113,13 @@ class Period
|
|
|
110
113
|
phrase = phrase.clean
|
|
111
114
|
case phrase
|
|
112
115
|
when /\Afrom\s+([^\s]+)\s+to\s+([^\s]+)(\s+per\s+[^\s]+)?\z/i
|
|
113
|
-
from_phrase =
|
|
114
|
-
to_phrase =
|
|
116
|
+
from_phrase = ::Regexp.last_match(1)
|
|
117
|
+
to_phrase = ::Regexp.last_match(2)
|
|
115
118
|
when /\Afrom\s+([^\s]+)(\s+per\s+[^\s]+)?\z/, /\Ato\s+([^\s]+)(\s+per\s+[^\s]+)?\z/i
|
|
116
|
-
from_phrase =
|
|
119
|
+
from_phrase = ::Regexp.last_match(1)
|
|
117
120
|
to_phrase = nil
|
|
118
121
|
when /\A([^\s]+)(\s+per\s+[^\s]+)?\z/
|
|
119
|
-
from_phrase =
|
|
122
|
+
from_phrase = ::Regexp.last_match(1)
|
|
120
123
|
to_phrase = nil
|
|
121
124
|
else
|
|
122
125
|
raise ArgumentError, "unintelligible period phrase: '#{phrase}''"
|
|
@@ -183,7 +186,7 @@ class Period
|
|
|
183
186
|
"Period(#{first.iso}..#{last.iso})"
|
|
184
187
|
end
|
|
185
188
|
|
|
186
|
-
# Allow erb documents
|
|
189
|
+
# Allow erb documents to directly interpolate ranges
|
|
187
190
|
def tex_quote
|
|
188
191
|
"#{first.iso}--#{last.iso}"
|
|
189
192
|
end
|
|
@@ -205,11 +208,6 @@ class Period
|
|
|
205
208
|
[first, last] <=> [other.first, other.last]
|
|
206
209
|
end
|
|
207
210
|
|
|
208
|
-
# Comparable does not include this.
|
|
209
|
-
def !=(other)
|
|
210
|
-
!(self == other)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
211
|
# Return the hash value for this Period. Make Period's with identical
|
|
214
212
|
# values test eql? so that they may be used as hash keys.
|
|
215
213
|
#
|
|
@@ -224,18 +222,6 @@ class Period
|
|
|
224
222
|
hash == other.hash
|
|
225
223
|
end
|
|
226
224
|
|
|
227
|
-
# Return whether this Period contains the given date.
|
|
228
|
-
#
|
|
229
|
-
# @param date [Date] date to test
|
|
230
|
-
# @return [Boolean] is the given date within this Period?
|
|
231
|
-
def contains?(date)
|
|
232
|
-
date = date.to_date if date.respond_to?(:to_date)
|
|
233
|
-
raise ArgumentError, 'argument must be a Date' unless date.is_a?(Date)
|
|
234
|
-
|
|
235
|
-
to_range.cover?(date)
|
|
236
|
-
end
|
|
237
|
-
alias_method :===, :contains?
|
|
238
|
-
|
|
239
225
|
include Enumerable
|
|
240
226
|
|
|
241
227
|
# @group Enumeration
|
|
@@ -347,38 +333,47 @@ class Period
|
|
|
347
333
|
|
|
348
334
|
# Return a period representing a chunk containing a given Date.
|
|
349
335
|
def self.day_containing(date)
|
|
336
|
+
date = Date.ensure_date(date)
|
|
350
337
|
Period.new(date, date)
|
|
351
338
|
end
|
|
352
339
|
|
|
353
340
|
def self.week_containing(date)
|
|
341
|
+
date = Date.ensure_date(date)
|
|
354
342
|
Period.new(date.beginning_of_week, date.end_of_week)
|
|
355
343
|
end
|
|
356
344
|
|
|
357
345
|
def self.biweek_containing(date)
|
|
346
|
+
date = Date.ensure_date(date)
|
|
358
347
|
Period.new(date.beginning_of_biweek, date.end_of_biweek)
|
|
359
348
|
end
|
|
360
349
|
|
|
361
350
|
def self.semimonth_containing(date)
|
|
351
|
+
date = Date.ensure_date(date)
|
|
362
352
|
Period.new(date.beginning_of_semimonth, date.end_of_semimonth)
|
|
363
353
|
end
|
|
364
354
|
|
|
365
355
|
def self.month_containing(date)
|
|
356
|
+
date = Date.ensure_date(date)
|
|
366
357
|
Period.new(date.beginning_of_month, date.end_of_month)
|
|
367
358
|
end
|
|
368
359
|
|
|
369
360
|
def self.bimonth_containing(date)
|
|
361
|
+
date = Date.ensure_date(date)
|
|
370
362
|
Period.new(date.beginning_of_bimonth, date.end_of_bimonth)
|
|
371
363
|
end
|
|
372
364
|
|
|
373
365
|
def self.quarter_containing(date)
|
|
366
|
+
date = Date.ensure_date(date)
|
|
374
367
|
Period.new(date.beginning_of_quarter, date.end_of_quarter)
|
|
375
368
|
end
|
|
376
369
|
|
|
377
370
|
def self.half_containing(date)
|
|
371
|
+
date = Date.ensure_date(date)
|
|
378
372
|
Period.new(date.beginning_of_half, date.end_of_half)
|
|
379
373
|
end
|
|
380
374
|
|
|
381
375
|
def self.year_containing(date)
|
|
376
|
+
date = Date.ensure_date(date)
|
|
382
377
|
Period.new(date.beginning_of_year, date.end_of_year)
|
|
383
378
|
end
|
|
384
379
|
|
|
@@ -628,8 +623,7 @@ class Period
|
|
|
628
623
|
# @param round_up_last [Boolean] allow the last period in the returned array
|
|
629
624
|
# to extend beyond the end of self.
|
|
630
625
|
# @return [Array<Period>] periods that subdivide self into chunks of size, `size`
|
|
631
|
-
def chunks(size: :month, partial_first: true, partial_last: true,
|
|
632
|
-
round_up_last: false)
|
|
626
|
+
def chunks(size: :month, partial_first: true, partial_last: true, round_up_last: false)
|
|
633
627
|
chunk_size = size.to_sym
|
|
634
628
|
raise ArgumentError, "unknown chunk size '#{chunk_size}'" unless CHUNKS.include?(chunk_size)
|
|
635
629
|
|
|
@@ -819,6 +813,19 @@ class Period
|
|
|
819
813
|
to_range.overlaps?(other.to_range)
|
|
820
814
|
end
|
|
821
815
|
|
|
816
|
+
# @group Coverage
|
|
817
|
+
|
|
818
|
+
# Return whether this Period contains the given date.
|
|
819
|
+
#
|
|
820
|
+
# @param date [Date] date to test
|
|
821
|
+
# @return [Boolean] is the given date within this Period?
|
|
822
|
+
def contains?(date)
|
|
823
|
+
date = Date.ensure(date)
|
|
824
|
+
|
|
825
|
+
to_range.cover?(date)
|
|
826
|
+
end
|
|
827
|
+
alias_method :===, :contains?
|
|
828
|
+
|
|
822
829
|
# Return whether any of the given periods overlap any other.
|
|
823
830
|
#
|
|
824
831
|
# @example
|
|
@@ -894,4 +901,212 @@ class Period
|
|
|
894
901
|
to_range.gaps(periods.map(&:to_range))
|
|
895
902
|
.map { |r| Period.new(r.first, r.last) }
|
|
896
903
|
end
|
|
904
|
+
|
|
905
|
+
# Return the fraction of a whole year represented by the part of this Period
|
|
906
|
+
# from its beginning to the given date2, following the day-count convention,
|
|
907
|
+
# convention.
|
|
908
|
+
#
|
|
909
|
+
# This Period is assumed to represent an entire accrual period, for example,
|
|
910
|
+
# a whole month for a loan, or a whole quarter or semi-year for a bond. We
|
|
911
|
+
# infer the annual frequency of payments from the length of this Period.
|
|
912
|
+
#
|
|
913
|
+
# Return the fraction of the year that has elapsed on date2, typically the
|
|
914
|
+
# settlement date.
|
|
915
|
+
#
|
|
916
|
+
# - ~first~ :: is the date on which interest accrual begins
|
|
917
|
+
# - ~date2~ :: is the date when payment of interest is made,
|
|
918
|
+
# - ~last~ :: is the date on which the interest accrual period ends,
|
|
919
|
+
# usually irrelevant unless actual days in period are considered
|
|
920
|
+
#
|
|
921
|
+
# The ~convention~ is the day-count convention to be used.
|
|
922
|
+
#
|
|
923
|
+
# That is, the convention for how interest is calculated between
|
|
924
|
+
# interest payment dates. This has to do with the fraction by which the
|
|
925
|
+
# annual interest or coupon rate is multiplied to pay for a partial
|
|
926
|
+
# period. The numerator is the number of days that have elapsed; The
|
|
927
|
+
# denominator is the number of days in the entire year. These involve
|
|
928
|
+
# assumptions about the number of days in the months that fall within
|
|
929
|
+
# the partial period. They will assume either that every month consists
|
|
930
|
+
# of 30 days, even if the particular period in which the payment occurs
|
|
931
|
+
# contains February, July, and August, or they will take into account
|
|
932
|
+
# the actual number of days in the particular period.
|
|
933
|
+
#
|
|
934
|
+
# | ~convention~ | Description | Source |
|
|
935
|
+
# |--------------+--------------------+------------------------------------|
|
|
936
|
+
# | 0 | 30/360 Bond | ISDA 2006 Section 4.16(f) |
|
|
937
|
+
# | 1 | 30/360 US | ISDA 2006 Section 4.16(f) (Note A) |
|
|
938
|
+
# | 2 | 30E/360 | ISDA 2006 Section 4.16(g) |
|
|
939
|
+
# | 3 | 30E/360 ISDA | ISDA 2006 Section 4.16(h) |
|
|
940
|
+
# | 4 | Actual/actual ICMA | ISDA 2006 Section 4.16(c) |
|
|
941
|
+
# | 5 | Actual/actual ISDA | ISDA 2006 Section 4.16(b) |
|
|
942
|
+
# | 6 | Actual/365 Fixed | ISDA 2006 Section 4.16(d) |
|
|
943
|
+
# | 7 | Actual/360 | ISDA 2006 Section 4.16(e) |
|
|
944
|
+
# | 8 | Actual/364 | |
|
|
945
|
+
# | 9 | Actual/365L | ICMA Rule 251.1(i) |
|
|
946
|
+
# | 10 | Actual/Actual AFB | |
|
|
947
|
+
# | 11 | 1/1 | ISDA 2006 Section 4.16(a) |
|
|
948
|
+
#
|
|
949
|
+
# Source: https://en.wikipedia.org/wiki/Day_count_convention
|
|
950
|
+
#
|
|
951
|
+
# Unlike Period#size, day counts use elapsed days, without adding one.
|
|
952
|
+
# For ICMA, first and last are consecutive coupon boundaries; only regular
|
|
953
|
+
# coupon periods are supported. Settlement must lie within this Period.
|
|
954
|
+
# Actual/365L uses the settlement date to determine its denominator, following
|
|
955
|
+
# the reference above. Convention 11 (1/1) always returns one, even for equal dates.
|
|
956
|
+
# AFB counts whole years backwards, with the February month-end adjustment
|
|
957
|
+
# used by QuantLib, then counts the remaining days (start included, end excluded).
|
|
958
|
+
#
|
|
959
|
+
# @param date2 [Date, String] payment or settlement date within this Period
|
|
960
|
+
# @param convention [Integer] day-count convention, 0 through 11; defaults to 1 (30/360 US)
|
|
961
|
+
# @param eom [Boolean] apply February end-of-month rules for convention 1
|
|
962
|
+
# @param maturity_date [Date, String, nil] final maturity for convention 3
|
|
963
|
+
# @return [BigDecimal] accrued fraction of a year
|
|
964
|
+
def year_fraction(date2, convention: 1, eom: true, maturity_date: nil)
|
|
965
|
+
unless convention.is_a?(Integer) && (0..11).cover?(convention)
|
|
966
|
+
raise ArgumentError, 'convention must be an integer 0..11'
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
date1 = first
|
|
970
|
+
date2 = Date.ensure(date2)
|
|
971
|
+
unless (first..last).cover?(date2)
|
|
972
|
+
raise ArgumentError, 'settlement date must lie within the period'
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
maturity_date = Date.ensure(maturity_date) if maturity_date
|
|
976
|
+
return BigDecimal(1) if convention == 11
|
|
977
|
+
return BigDecimal(0) if date1 == date2
|
|
978
|
+
|
|
979
|
+
if (0..3).cover?(convention)
|
|
980
|
+
# 360-day methods
|
|
981
|
+
y1 = date1.year
|
|
982
|
+
m1 = date1.month
|
|
983
|
+
d1 = date1.day
|
|
984
|
+
y2 = date2.year
|
|
985
|
+
m2 = date2.month
|
|
986
|
+
d2 = date2.day
|
|
987
|
+
case convention
|
|
988
|
+
when 0
|
|
989
|
+
# 30/360 Bond
|
|
990
|
+
d1 = [d1, 30].min
|
|
991
|
+
d2 = d1 > 29 ? [d2, 30].min : d2
|
|
992
|
+
when 1
|
|
993
|
+
# 30/360 US
|
|
994
|
+
if eom && date1.end_of_month? && m1 == 2 && date2.end_of_month? && m2 == 2
|
|
995
|
+
d2 = 30
|
|
996
|
+
end
|
|
997
|
+
if eom && date1.end_of_month? && m1 == 2
|
|
998
|
+
d1 = 30
|
|
999
|
+
end
|
|
1000
|
+
# If D2 is 31 and D1 is 30 or 31, then change D2 to 30.
|
|
1001
|
+
if d2 == 31 && (d1 == 30 || d1 == 31)
|
|
1002
|
+
d2 = 30
|
|
1003
|
+
end
|
|
1004
|
+
# If D1 is 31, then change D1 to 30.
|
|
1005
|
+
if d1 == 31
|
|
1006
|
+
d1 = 30
|
|
1007
|
+
end
|
|
1008
|
+
when 2
|
|
1009
|
+
# 30E/360
|
|
1010
|
+
# If D1 is 31, then change D1 to 30.
|
|
1011
|
+
if d1 == 31
|
|
1012
|
+
d1 = 30
|
|
1013
|
+
end
|
|
1014
|
+
# If D2 is 31, then change D2 to 30.
|
|
1015
|
+
if d2 == 31
|
|
1016
|
+
d2 = 30
|
|
1017
|
+
end
|
|
1018
|
+
when 3
|
|
1019
|
+
# 30E/360 ISDA
|
|
1020
|
+
if date1.end_of_month?
|
|
1021
|
+
d1 = 30
|
|
1022
|
+
end
|
|
1023
|
+
if date2.end_of_month? && !(m2 == 2 && maturity_date && date2 == maturity_date)
|
|
1024
|
+
# If D2 is the last day of the month (unless Date2 is the maturity
|
|
1025
|
+
# date and M2 is February), then change D2 to 30.
|
|
1026
|
+
d2 = 30
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
num = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
|
|
1030
|
+
return BigDecimal(num) / 360
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
num = BigDecimal((date2 - date1).to_i)
|
|
1034
|
+
case convention
|
|
1035
|
+
when 4
|
|
1036
|
+
# Actual/actual ICMA: elapsed days in the reference coupon period.
|
|
1037
|
+
num / (freq_per_year(days: (last - first).to_i) * (last - first).to_i)
|
|
1038
|
+
when 5
|
|
1039
|
+
# Actual/actual ISDA: split only the elapsed interval at year boundaries.
|
|
1040
|
+
frac = BigDecimal(0)
|
|
1041
|
+
cursor = date1
|
|
1042
|
+
while cursor < date2
|
|
1043
|
+
boundary = [Date.new(cursor.year + 1, 1, 1), date2].min
|
|
1044
|
+
frac += BigDecimal((boundary - cursor).to_i) / (cursor.leap? ? 366 : 365)
|
|
1045
|
+
cursor = boundary
|
|
1046
|
+
end
|
|
1047
|
+
frac
|
|
1048
|
+
when 6
|
|
1049
|
+
num / 365
|
|
1050
|
+
when 7
|
|
1051
|
+
num / 360
|
|
1052
|
+
when 8
|
|
1053
|
+
num / 364
|
|
1054
|
+
when 9
|
|
1055
|
+
# Actual/365L: annual coupons exclude the start and include the end
|
|
1056
|
+
# when looking for February 29. Other frequencies use the settlement year.
|
|
1057
|
+
leap =
|
|
1058
|
+
if freq_per_year(days: (last - first).to_i) == 1
|
|
1059
|
+
(first.year..date2.year).any? do |year|
|
|
1060
|
+
Date.leap?(year) && first < Date.new(year, 2, 29) && Date.new(year, 2, 29) <= date2
|
|
1061
|
+
end
|
|
1062
|
+
else
|
|
1063
|
+
date2.leap?
|
|
1064
|
+
end
|
|
1065
|
+
num / (leap ? 366 : 365)
|
|
1066
|
+
when 10
|
|
1067
|
+
# Actual/actual AFB: whole years plus the remaining fraction.
|
|
1068
|
+
years = 0
|
|
1069
|
+
remainder_end = date2
|
|
1070
|
+
loop do
|
|
1071
|
+
previous = remainder_end << 12
|
|
1072
|
+
previous += 1 if previous.month == 2 && previous.day == 28 && previous.leap?
|
|
1073
|
+
break if previous < date1
|
|
1074
|
+
|
|
1075
|
+
years += 1
|
|
1076
|
+
remainder_end = previous
|
|
1077
|
+
end
|
|
1078
|
+
leap = (date1.year..remainder_end.year).any? do |year|
|
|
1079
|
+
Date.leap?(year) && date1 <= Date.new(year, 2, 29) && Date.new(year, 2, 29) < remainder_end
|
|
1080
|
+
end
|
|
1081
|
+
years + BigDecimal((remainder_end - date1).to_i) / (leap ? 366 : 365)
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
# Infer payment frequency from the period length, allowing the same tolerance
|
|
1086
|
+
# as chunk_name. Daily periods use the number of days in the starting year.
|
|
1087
|
+
# @param days [Integer] period length; use elapsed days for coupon boundaries
|
|
1088
|
+
def freq_per_year(days: length)
|
|
1089
|
+
case Period.days_to_chunk(days)
|
|
1090
|
+
when :year
|
|
1091
|
+
1
|
|
1092
|
+
when :half
|
|
1093
|
+
2
|
|
1094
|
+
when :quarter
|
|
1095
|
+
4
|
|
1096
|
+
when :bimonth
|
|
1097
|
+
6
|
|
1098
|
+
when :month
|
|
1099
|
+
12
|
|
1100
|
+
when :semimonth
|
|
1101
|
+
24
|
|
1102
|
+
when :biweek
|
|
1103
|
+
26
|
|
1104
|
+
when :week
|
|
1105
|
+
52
|
|
1106
|
+
when :day
|
|
1107
|
+
first.leap? ? 366 : 365
|
|
1108
|
+
else
|
|
1109
|
+
raise ArgumentError, "indeterminate size of period (#{self})"
|
|
1110
|
+
end
|
|
1111
|
+
end
|
|
897
1112
|
end
|
data/lib/fat_period/version.rb
CHANGED
data/lib/fat_period.rb
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
4
|
-
require '
|
|
5
|
-
require 'fat_period/period'
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'active_support'
|
|
6
5
|
|
|
7
|
-
require 'fat_core/date'
|
|
8
6
|
require 'fat_core/range'
|
|
9
7
|
require 'fat_core/string'
|
|
8
|
+
require 'fat_date'
|
|
9
|
+
|
|
10
|
+
# Gem Overview (extracted from README.org by gem_docs)
|
|
11
|
+
#
|
|
12
|
+
# * Introduction
|
|
13
|
+
# ~FatPeriod~ provides a Ruby ~Period~ class for dealing with time periods of
|
|
14
|
+
# days, that is ranges whose endpoints are of class ~Date~. It's target is
|
|
15
|
+
# financial applications, but it serves well for any application where periods
|
|
16
|
+
# of time are useful. It builds on the [[https://github.com/ddoherty03/fat_date][fat_date]] gem, which provides
|
|
17
|
+
# enhancements to the ~Date~ class, especially its class method ~Date.spec~ for
|
|
18
|
+
# interpreting a rich set of "specs" as the beginning or end of a variety of
|
|
19
|
+
# calendar-related periods.
|
|
20
|
+
#
|
|
21
|
+
# In addition, set operations are provided for Period, as well as methods for
|
|
22
|
+
# breaking a larger periods into an array of smaller periods of various 'chunk'
|
|
23
|
+
# sizes that correspond to calendar-related periods such as days, weeks, months,
|
|
24
|
+
# and so forth.
|
|
25
|
+
module FatPeriod
|
|
26
|
+
require 'fat_period/version'
|
|
27
|
+
require 'fat_period/date'
|
|
28
|
+
require 'fat_period/period'
|
|
29
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fat_period
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel E. Doherty
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: fat_core
|
|
@@ -16,15 +15,28 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '7.0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
27
|
-
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: fat_date
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
28
40
|
email:
|
|
29
41
|
- ded@ddoherty.net
|
|
30
42
|
executables: []
|
|
@@ -32,12 +44,14 @@ extensions: []
|
|
|
32
44
|
extra_rdoc_files: []
|
|
33
45
|
files:
|
|
34
46
|
- ".envrc"
|
|
35
|
-
- ".github/workflows/
|
|
47
|
+
- ".github/workflows/ruby.yml"
|
|
36
48
|
- ".gitignore"
|
|
37
49
|
- ".rspec"
|
|
38
|
-
- ".
|
|
50
|
+
- ".rubocop.yml"
|
|
51
|
+
- CHANGELOG.org
|
|
39
52
|
- Gemfile
|
|
40
53
|
- LICENSE.txt
|
|
54
|
+
- README.md
|
|
41
55
|
- README.org
|
|
42
56
|
- Rakefile
|
|
43
57
|
- bin/console
|
|
@@ -50,7 +64,6 @@ files:
|
|
|
50
64
|
homepage: https://github.com/ddoherty03/fat_period
|
|
51
65
|
licenses: []
|
|
52
66
|
metadata: {}
|
|
53
|
-
post_install_message:
|
|
54
67
|
rdoc_options: []
|
|
55
68
|
require_paths:
|
|
56
69
|
- lib
|
|
@@ -65,8 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
65
78
|
- !ruby/object:Gem::Version
|
|
66
79
|
version: '0'
|
|
67
80
|
requirements: []
|
|
68
|
-
rubygems_version: 3.
|
|
69
|
-
signing_key:
|
|
81
|
+
rubygems_version: 3.6.2
|
|
70
82
|
specification_version: 4
|
|
71
83
|
summary: Implements a Period class as a Range of Dates.
|
|
72
84
|
test_files: []
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
name: Ruby Gem
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ "master" ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ "master" ]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
name: Build + Publish
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
permissions:
|
|
14
|
-
contents: read
|
|
15
|
-
packages: write
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
- name: Set up Ruby 2.6
|
|
20
|
-
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
|
21
|
-
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
|
22
|
-
# uses: ruby/setup-ruby@v1
|
|
23
|
-
uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
|
|
24
|
-
with:
|
|
25
|
-
ruby-version: 2.6.x
|
|
26
|
-
|
|
27
|
-
- name: Publish to GPR
|
|
28
|
-
run: |
|
|
29
|
-
mkdir -p $HOME/.gem
|
|
30
|
-
touch $HOME/.gem/credentials
|
|
31
|
-
chmod 0600 $HOME/.gem/credentials
|
|
32
|
-
printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
33
|
-
gem build *.gemspec
|
|
34
|
-
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
|
35
|
-
env:
|
|
36
|
-
GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
|
|
37
|
-
OWNER: ${{ github.repository_owner }}
|
|
38
|
-
|
|
39
|
-
- name: Publish to RubyGems
|
|
40
|
-
run: |
|
|
41
|
-
mkdir -p $HOME/.gem
|
|
42
|
-
touch $HOME/.gem/credentials
|
|
43
|
-
chmod 0600 $HOME/.gem/credentials
|
|
44
|
-
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
45
|
-
gem build *.gemspec
|
|
46
|
-
gem push *.gem
|
|
47
|
-
env:
|
|
48
|
-
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|