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 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: []