fat_period 1.0.3 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -3
- data/fat_period.gemspec +1 -1
- data/lib/fat_period/period.rb +101 -114
- data/lib/fat_period/version.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 609274b0338a91caf02d5fbf0ed37b4d2621dea51e93a393f9bde86e5287b0ad
|
4
|
+
data.tar.gz: cddf0aa8c7087d0705917319090dab8fea710579b697cc79d231f0257a80271d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3da42e489a2dc3a1d8b42191e589e675a71466048a70b1b2d04f430b2f3f34685f0c137194952a7c4ec33c117cf227b1143670409353351af9b8531492efc93b
|
7
|
+
data.tar.gz: e91123fa17ff8a9f47553652a302316418aaf2f5fe5eee587b45ecb9b27799ad20b39b9e1af350e4453ee69f19f4a8cd6a595ed82df2247c5bc8084c664768de
|
data/.travis.yml
CHANGED
data/fat_period.gemspec
CHANGED
data/lib/fat_period/period.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
1
|
require 'fat_core/date'
|
4
2
|
require 'fat_core/range'
|
5
3
|
require 'fat_core/string'
|
@@ -29,37 +27,10 @@ class Period
|
|
29
27
|
# @raise [ArgumentError] if first date is later than last date
|
30
28
|
# @return [Period]
|
31
29
|
def initialize(first, last)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
begin
|
36
|
-
@first = Date.parse(first.to_s)
|
37
|
-
rescue ArgumentError => e
|
38
|
-
if e.message =~ /invalid date/
|
39
|
-
raise ArgumentError, "invalid date '#{first}'"
|
40
|
-
end
|
41
|
-
|
42
|
-
raise
|
43
|
-
end
|
44
|
-
else
|
45
|
-
raise ArgumentError, 'use Date or String to initialize Period'
|
46
|
-
end
|
30
|
+
@first = Date.ensure_date(first).freeze
|
31
|
+
@last = Date.ensure_date(last).freeze
|
32
|
+
freeze
|
47
33
|
|
48
|
-
if last.is_a?(Date)
|
49
|
-
@last = last
|
50
|
-
elsif last.respond_to?(:to_s)
|
51
|
-
begin
|
52
|
-
@last = Date.parse(last.to_s)
|
53
|
-
rescue ArgumentError => e
|
54
|
-
if e.message =~ /invalid date/
|
55
|
-
raise ArgumentError, "you gave an invalid date '#{last}'"
|
56
|
-
end
|
57
|
-
|
58
|
-
raise
|
59
|
-
end
|
60
|
-
else
|
61
|
-
raise ArgumentError, 'use Date or String to initialize Period'
|
62
|
-
end
|
63
34
|
return unless @first > @last
|
64
35
|
|
65
36
|
raise ArgumentError, "Period's first date is later than its last date"
|
@@ -116,14 +87,13 @@ class Period
|
|
116
87
|
# @return [Period] translated from phrase
|
117
88
|
def self.parse_phrase(phrase)
|
118
89
|
phrase = phrase.clean
|
119
|
-
|
90
|
+
case phrase
|
91
|
+
when /\Afrom (.*) to (.*)\z/
|
120
92
|
from_phrase = $1
|
121
93
|
to_phrase = $2
|
122
|
-
|
94
|
+
when /\Afrom (.*)\z/, /\Ato (.*)\z/
|
123
95
|
from_phrase = $1
|
124
96
|
to_phrase = nil
|
125
|
-
elsif phrase =~ /\Ato (.*)\z/
|
126
|
-
from_phrase = $1
|
127
97
|
else
|
128
98
|
from_phrase = phrase
|
129
99
|
to_phrase = nil
|
@@ -208,6 +178,20 @@ class Period
|
|
208
178
|
!(self == other)
|
209
179
|
end
|
210
180
|
|
181
|
+
# Return the hash value for this Period. Make Period's with identical
|
182
|
+
# values test eql? so that they may be used as hash keys.
|
183
|
+
#
|
184
|
+
# @return [Integer]
|
185
|
+
def hash
|
186
|
+
(first.hash | last.hash)
|
187
|
+
end
|
188
|
+
|
189
|
+
def eql?(other)
|
190
|
+
return nil unless other.is_a?(Period)
|
191
|
+
|
192
|
+
hash == other.hash
|
193
|
+
end
|
194
|
+
|
211
195
|
# Return whether this Period contains the given date.
|
212
196
|
#
|
213
197
|
# @param date [Date] date to test
|
@@ -297,6 +281,12 @@ class Period
|
|
297
281
|
CHUNKS = %i[day week biweek semimonth month bimonth quarter
|
298
282
|
half year irregular].freeze
|
299
283
|
|
284
|
+
CHUNK_ORDER = {}
|
285
|
+
CHUNKS.each_with_index do |c, i|
|
286
|
+
CHUNK_ORDER[c] = i
|
287
|
+
end
|
288
|
+
CHUNK_ORDER.freeze
|
289
|
+
|
300
290
|
# An Array of Ranges for the number of days that can be covered by each chunk.
|
301
291
|
CHUNK_RANGE = {
|
302
292
|
day: (1..1), week: (7..7), biweek: (14..14), semimonth: (15..16),
|
@@ -304,6 +294,10 @@ class Period
|
|
304
294
|
half: (180..183), year: (365..366)
|
305
295
|
}.freeze
|
306
296
|
|
297
|
+
def self.chunk_cmp(chunk1, chunk2)
|
298
|
+
CHUNK_ORDER[chunk1] <=> CHUNK_ORDER[chunk2]
|
299
|
+
end
|
300
|
+
|
307
301
|
# Return a period representing a chunk containing a given Date.
|
308
302
|
def self.day_containing(date)
|
309
303
|
Period.new(date, date)
|
@@ -341,6 +335,17 @@ class Period
|
|
341
335
|
Period.new(date.beginning_of_year, date.end_of_year)
|
342
336
|
end
|
343
337
|
|
338
|
+
def self.chunk_containing(date, chunk)
|
339
|
+
raise ArgumentError, 'chunk is nil' unless chunk
|
340
|
+
|
341
|
+
chunk = chunk.to_sym
|
342
|
+
raise ArgumentError, "unknown chunk name: #{chunk}" unless CHUNKS.include?(chunk)
|
343
|
+
|
344
|
+
date = Date.ensure_date(date)
|
345
|
+
method = "#{chunk}_containing".to_sym
|
346
|
+
send(method, date)
|
347
|
+
end
|
348
|
+
|
344
349
|
# Return a Period representing a chunk containing today.
|
345
350
|
def self.this_day
|
346
351
|
day_containing(Date.current)
|
@@ -461,20 +466,20 @@ class Period
|
|
461
466
|
end
|
462
467
|
end
|
463
468
|
|
464
|
-
# Return the chunk symbol represented by the number of days given, but allow
|
465
|
-
# deviation from the minimum and maximum number of days for periods larger
|
466
|
-
# than bimonths. The default tolerance is +/-10%, but that can be
|
467
|
-
# reason for allowing a bit of tolerance for the larger
|
468
|
-
# financial statements meant to cover a given calendar
|
469
|
-
# or long by a few days due to such things as
|
470
|
-
# accounting convenience. For example, a bank might
|
471
|
-
# statements approximately every 30 days, but issue them
|
472
|
-
# avoid having the closing date fall on a weekend or
|
473
|
-
# be able to recognize them as "monthly", even
|
474
|
-
# be a few days shorter or longer than any
|
475
|
-
# eliminate this "fudge factor" by setting
|
476
|
-
# the number of days corresponds to none of
|
477
|
-
# return the symbol `:irregular`.
|
469
|
+
# Return the chunk symbol represented by the number of days given, but allow
|
470
|
+
# a deviation from the minimum and maximum number of days for periods larger
|
471
|
+
# than bimonths. The default tolerance is +/-10%, but that can be
|
472
|
+
# adjusted. The reason for allowing a bit of tolerance for the larger
|
473
|
+
# periods is that financial statements meant to cover a given calendar
|
474
|
+
# period are often short or long by a few days due to such things as
|
475
|
+
# weekends, holidays, or accounting convenience. For example, a bank might
|
476
|
+
# issuer "monthly" statements approximately every 30 days, but issue them
|
477
|
+
# earlier or later to avoid having the closing date fall on a weekend or
|
478
|
+
# holiday. We still want to be able to recognize them as "monthly", even
|
479
|
+
# though the period covered might be a few days shorter or longer than any
|
480
|
+
# possible calendar month. You can eliminate this "fudge factor" by setting
|
481
|
+
# the `tolerance_pct` to zero. If the number of days corresponds to none of
|
482
|
+
# the defined calendar periods, return the symbol `:irregular`.
|
478
483
|
#
|
479
484
|
# @example
|
480
485
|
# Period.days_to_chunk(360) #=> :year
|
@@ -483,12 +488,12 @@ class Period
|
|
483
488
|
# Period.days_to_chunk(88, 0) #=> :irregular
|
484
489
|
#
|
485
490
|
# @param days [Integer] the number of days in the period under test
|
486
|
-
# @param tolerance_pct [
|
491
|
+
# @param tolerance_pct [Numeric] the percent deviation allowed, e.g. 10 => 10%
|
487
492
|
# @return [Symbol] symbol for the period corresponding to days number of days
|
488
493
|
def self.days_to_chunk(days, tolerance_pct = 10)
|
489
494
|
result = :irregular
|
490
495
|
CHUNK_RANGE.each_pair do |chunk, rng|
|
491
|
-
if [
|
496
|
+
if %i[semimonth biweek week day].include?(chunk)
|
492
497
|
# Be strict for shorter periods.
|
493
498
|
if rng.cover?(days)
|
494
499
|
result = chunk
|
@@ -578,77 +583,59 @@ class Period
|
|
578
583
|
# @return [Array<Period>] periods that subdivide self into chunks of size, `size`
|
579
584
|
def chunks(size: :month, partial_first: false, partial_last: false,
|
580
585
|
round_up_last: false)
|
581
|
-
|
582
|
-
unless CHUNKS.include?(
|
583
|
-
raise ArgumentError, "unknown chunk size '#{size}'"
|
584
|
-
end
|
586
|
+
chunk_size = size.to_sym
|
587
|
+
raise ArgumentError, "unknown chunk size '#{chunk_size}'" unless CHUNKS.include?(chunk_size)
|
585
588
|
|
586
|
-
|
587
|
-
|
589
|
+
containing_period = Period.chunk_containing(first, chunk_size)
|
590
|
+
return [dup] if self == containing_period
|
588
591
|
|
589
|
-
|
590
|
-
|
592
|
+
# Period too small for even a single chunk and is wholly-contained by a
|
593
|
+
# single chunk.
|
594
|
+
result = []
|
595
|
+
if proper_subset_of?(containing_period)
|
596
|
+
result =
|
597
|
+
if partial_first || partial_last
|
598
|
+
if round_up_last
|
599
|
+
[containing_period]
|
600
|
+
else
|
601
|
+
[dup]
|
602
|
+
end
|
603
|
+
else
|
604
|
+
[]
|
605
|
+
end
|
606
|
+
return result
|
591
607
|
end
|
592
608
|
|
593
|
-
result = []
|
594
609
|
chunk_start = first.dup
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
chunk_end = chunk_start.end_of_quarter
|
612
|
-
when :bimonth
|
613
|
-
unless partial_first
|
614
|
-
chunk_start += 1.day until chunk_start.beginning_of_bimonth?
|
615
|
-
end
|
616
|
-
chunk_end = (chunk_start.end_of_month + 1.day).end_of_month
|
617
|
-
when :month
|
618
|
-
unless partial_first
|
619
|
-
chunk_start += 1.day until chunk_start.beginning_of_month?
|
620
|
-
end
|
621
|
-
chunk_end = chunk_start.end_of_month
|
622
|
-
when :semimonth
|
623
|
-
unless partial_first
|
624
|
-
chunk_start += 1.day until chunk_start.beginning_of_semimonth?
|
625
|
-
end
|
626
|
-
chunk_end = chunk_start.end_of_semimonth
|
627
|
-
when :biweek
|
628
|
-
unless partial_first
|
629
|
-
chunk_start += 1.day until chunk_start.beginning_of_biweek?
|
630
|
-
end
|
631
|
-
chunk_end = chunk_start.end_of_biweek
|
632
|
-
when :week
|
633
|
-
unless partial_first
|
634
|
-
chunk_start += 1.day until chunk_start.beginning_of_week?
|
635
|
-
end
|
636
|
-
chunk_end = chunk_start.end_of_week
|
637
|
-
when :day
|
638
|
-
chunk_end = chunk_start
|
639
|
-
else
|
640
|
-
raise ArgumentError, "invalid chunk size '#{size}'"
|
641
|
-
end
|
642
|
-
if chunk_end <= last
|
643
|
-
result << Period.new(chunk_start, chunk_end)
|
644
|
-
elsif round_up_last
|
610
|
+
chunk_end = chunk_start.end_of_chunk(chunk_size)
|
611
|
+
if chunk_start.beginning_of_chunk?(chunk_size) || partial_first
|
612
|
+
# Keep the first chunk if it's whole or partials allowed
|
613
|
+
result << Period.new(chunk_start, chunk_end)
|
614
|
+
end
|
615
|
+
chunk_start = chunk_end + 1.day
|
616
|
+
chunk_end = chunk_start.end_of_chunk(chunk_size)
|
617
|
+
# Add Whole chunks
|
618
|
+
while chunk_end <= last
|
619
|
+
result << Period.new(chunk_start, chunk_end)
|
620
|
+
chunk_start = chunk_end + 1.day
|
621
|
+
chunk_end = chunk_start.end_of_chunk(chunk_size)
|
622
|
+
end
|
623
|
+
# Possibly append the final chunk to result
|
624
|
+
if chunk_start < last
|
625
|
+
if round_up_last
|
645
626
|
result << Period.new(chunk_start, chunk_end)
|
646
627
|
elsif partial_last
|
647
628
|
result << Period.new(chunk_start, last)
|
648
629
|
else
|
649
|
-
|
630
|
+
result
|
650
631
|
end
|
651
|
-
|
632
|
+
end
|
633
|
+
if partial_last && !partial_first && result.empty?
|
634
|
+
# Catch the case where the period is too small to make a whole chunk and
|
635
|
+
# partial_first is false, so it did not get included as the initial
|
636
|
+
# partial chunk, yet a partial_last is allowed, so include the whole
|
637
|
+
# period as a partial chunk.
|
638
|
+
result << Period.new(first, last)
|
652
639
|
end
|
653
640
|
result
|
654
641
|
end
|
@@ -694,7 +681,7 @@ class Period
|
|
694
681
|
to_range.superset_of?(other.to_range)
|
695
682
|
end
|
696
683
|
|
697
|
-
# Does this period wholly contain but not coincident with `other`?
|
684
|
+
# Does this period wholly contain but is not coincident with `other`?
|
698
685
|
#
|
699
686
|
# @example
|
700
687
|
# Period.parse('2015').proper_superset_of?(Period.parse('2015-2Q')) #=> true
|
data/lib/fat_period/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fat_period
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel E. Doherty
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -100,15 +100,15 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 4.8.3
|
104
104
|
type: :runtime
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
111
|
-
description:
|
110
|
+
version: 4.8.3
|
111
|
+
description:
|
112
112
|
email:
|
113
113
|
- ded-law@ddoherty.net
|
114
114
|
executables: []
|
@@ -132,7 +132,7 @@ files:
|
|
132
132
|
homepage: https://github.com/ddoherty03/fat_period
|
133
133
|
licenses: []
|
134
134
|
metadata: {}
|
135
|
-
post_install_message:
|
135
|
+
post_install_message:
|
136
136
|
rdoc_options: []
|
137
137
|
require_paths:
|
138
138
|
- lib
|
@@ -147,8 +147,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
147
|
- !ruby/object:Gem::Version
|
148
148
|
version: '0'
|
149
149
|
requirements: []
|
150
|
-
rubygems_version: 3.
|
151
|
-
signing_key:
|
150
|
+
rubygems_version: 3.3.3
|
151
|
+
signing_key:
|
152
152
|
specification_version: 4
|
153
153
|
summary: Implements a Period class as a Range of Dates.
|
154
154
|
test_files: []
|