fat_period 1.0.3 → 1.2.1

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
  SHA256:
3
- metadata.gz: 8a2fde65d6d14d1a0c94d09b2097528ef2f23826388b1f3291ac7e7d0b3312af
4
- data.tar.gz: c14e872d1fb6abf9015a4acf3a3b5c21c23afe28e05f8f15ba7df6db921751d2
3
+ metadata.gz: 609274b0338a91caf02d5fbf0ed37b4d2621dea51e93a393f9bde86e5287b0ad
4
+ data.tar.gz: cddf0aa8c7087d0705917319090dab8fea710579b697cc79d231f0257a80271d
5
5
  SHA512:
6
- metadata.gz: 23ab6b2bc98abfc6d7609df56f93b7acf8683d29fdd13a54c7377b321ffc0a29d29c5498c327054ef5c970c5650b129757fca7a0b71db4132c4d5328a9b31c24
7
- data.tar.gz: d266e75e1c4caaecbd14952cad1ffbc6cea36e65c6d6b5264b353edc7c3b5604aa04709489c5f3ffa058f3d0d9a236ce1ca2d228283e907092166c19351ddc72
6
+ metadata.gz: 3da42e489a2dc3a1d8b42191e589e675a71466048a70b1b2d04f430b2f3f34685f0c137194952a7c4ec33c117cf227b1143670409353351af9b8531492efc93b
7
+ data.tar.gz: e91123fa17ff8a9f47553652a302316418aaf2f5fe5eee587b45ecb9b27799ad20b39b9e1af350e4453ee69f19f4a8cd6a595ed82df2247c5bc8084c664768de
data/.travis.yml CHANGED
@@ -1,8 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.3.0
4
- - 2.4
5
3
  - 2.5
6
4
  - 2.6
7
5
  - 2.7
8
- - ruby-head
6
+ - 3.0
data/fat_period.gemspec CHANGED
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency 'pry-doc'
28
28
  spec.add_development_dependency 'pry-byebug'
29
29
 
30
- spec.add_runtime_dependency 'fat_core'
30
+ spec.add_runtime_dependency 'fat_core', '>= 4.8.3'
31
31
  end
@@ -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
- if first.is_a?(Date)
33
- @first = first
34
- elsif first.respond_to?(:to_s)
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
- if phrase =~ /\Afrom (.*) to (.*)\z/
90
+ case phrase
91
+ when /\Afrom (.*) to (.*)\z/
120
92
  from_phrase = $1
121
93
  to_phrase = $2
122
- elsif phrase =~ /\Afrom (.*)\z/
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 a
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 adjusted. The
467
- # reason for allowing a bit of tolerance for the larger periods is that
468
- # financial statements meant to cover a given calendar period are often short
469
- # or long by a few days due to such things as weekends, holidays, or
470
- # accounting convenience. For example, a bank might issuer "monthly"
471
- # statements approximately every 30 days, but issue them earlier or later to
472
- # avoid having the closing date fall on a weekend or holiday. We still want to
473
- # be able to recognize them as "monthly", even though the period covered might
474
- # be a few days shorter or longer than any possible calendar month. You can
475
- # eliminate this "fudge factor" by setting the `tolerance_pct` to zero. If
476
- # the number of days corresponds to none of the defined calendar periods,
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 [Numberic] the percent deviation allowed, e.g. 10 => 10%
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 [:semimonth, :biweek, :week, :day].include?(chunk)
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
- size = size.to_sym
582
- unless CHUNKS.include?(size)
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
- if CHUNK_RANGE[size].first > length
587
- return [self] if partial_first || partial_last
589
+ containing_period = Period.chunk_containing(first, chunk_size)
590
+ return [dup] if self == containing_period
588
591
 
589
- msg = "any #{size} is longer than this period's #{length} days"
590
- raise ArgumentError, msg
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
- while chunk_start <= last
596
- case size
597
- when :year
598
- unless partial_first
599
- chunk_start += 1.day until chunk_start.beginning_of_year?
600
- end
601
- chunk_end = chunk_start.end_of_year
602
- when :half
603
- unless partial_first
604
- chunk_start += 1.day until chunk_start.beginning_of_half?
605
- end
606
- chunk_end = chunk_start.end_of_half
607
- when :quarter
608
- unless partial_first
609
- chunk_start += 1.day until chunk_start.beginning_of_quarter?
610
- end
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
- break
630
+ result
650
631
  end
651
- chunk_start = result.last.last + 1.day
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
@@ -1,3 +1,3 @@
1
1
  module FatPeriod
2
- VERSION = '1.0.3'.freeze
2
+ VERSION = '1.2.1'.freeze
3
3
  end
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.0.3
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: 2020-02-23 00:00:00.000000000 Z
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: '0'
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: '0'
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.0.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: []