fat_period 1.1.1 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05402b5173907362901a6890276937450921c418596ebe53e7d3576bcebd3d7b
4
- data.tar.gz: 4d78c11cb533cb6f45f8e6cd941887c8cc2f1645c4db5052ad543627228c3e73
3
+ metadata.gz: cb2c6b600f446fa2bff8080b8056a32ceb3fda88be68c03db076591753c4992e
4
+ data.tar.gz: f4d65daa079f338c2a1a7ab5a0d326ae9af884d819a72077f1f24e8128ed70de
5
5
  SHA512:
6
- metadata.gz: 5fcb751568bcf4c7f33ee03f4a60c55bbbf04def94475069383ed31400fc62dc8c46d70affa60374b0cace8016b1f60655275127a6cbbed36479f7cc86972879
7
- data.tar.gz: 8b5120f531483283c834531b7b3a8ddfbc0e7f816087cbcaa50338c74a06990aa6bce5d619ac0331976e05608b645406d6ca6723a3cfec8bc936f2584a4ccc40
6
+ metadata.gz: 3059659aed2c2000401a6fceca177b9b7ea633adaaa1012bcb20db1069845bdc7dff35b97a53289a498399d1bc2217ceffd06c4dae94377ce7a47710c71ab2af
7
+ data.tar.gz: ab0e1a8aebca6fc8acf0e952e9a48956767b03e7cd9071d6487b52e0a544519e6801b927ae3b2f5021e1fc3ac8aeb95114488192fcd343b265b235efc38a6784
data/.travis.yml CHANGED
@@ -3,4 +3,4 @@ rvm:
3
3
  - 2.5
4
4
  - 2.6
5
5
  - 2.7
6
- - ruby-head
6
+ - 3.0
data/fat_period.gemspec CHANGED
@@ -23,9 +23,9 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency 'bundler'
24
24
  spec.add_development_dependency 'rake'
25
25
  spec.add_development_dependency 'rspec'
26
+ spec.add_development_dependency 'debug', '>= 1.0.0'
26
27
  spec.add_development_dependency 'pry'
27
28
  spec.add_development_dependency 'pry-doc'
28
- spec.add_development_dependency 'pry-byebug'
29
29
 
30
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,8 +27,9 @@ 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
- @first = Date.ensure_date(first)
33
- @last = Date.ensure_date(last)
30
+ @first = Date.ensure_date(first).freeze
31
+ @last = Date.ensure_date(last).freeze
32
+ freeze
34
33
 
35
34
  return unless @first > @last
36
35
 
@@ -71,36 +70,59 @@ class Period
71
70
  Period.new(first, second) if first && second
72
71
  end
73
72
 
74
- # Return a period as in `Period.parse` from a String phrase in which the from
75
- # spec is introduced with 'from' and, optionally, the to spec is introduced
76
- # with 'to'. A phrase with only a to spec is treated the same as one with
77
- # only a from spec. If neither 'from' nor 'to' appear in phrase, treat the
78
- # whole string as a from spec.
73
+ PHRASE_RE = %r{\A
74
+ # One or both of from and to parts
75
+ ((from\s+(?<from_part>[-_a-z0-9]+)\s*)?
76
+ (to\s+(?<to_part>[-_a-z0-9]+))?)
77
+ # Wholly optional chunk part
78
+ (\s+per\s+(?<chunk_part>\w+))?\z}xi
79
+
80
+ # Return an array of periods, either a single period as in `Period.parse`
81
+ # from a String phrase in which a `from spec` is introduced with 'from' and,
82
+ # optionally, a `to spec` is introduced with 'to', or a number of periods if
83
+ # there is a 'per <chunk>' modifier. A phrase with only a `to spec` is
84
+ # treated the same as one with only a from spec. If neither 'from' nor 'to'
85
+ # appear in phrase, treat the whole string as a from spec.
79
86
  #
80
87
  # @example
81
- # Period.parse_phrase('from 2014-11 to 2015-3Q') #=> Period('2014-11-01..2015-09-30')
82
- # Period.parse_phrase('from 2014-11') #=> Period('2014-11-01..2014-11-30')
83
- # Period.parse_phrase('from 2015-3Q') #=> Period('2015-09-01..2015-12-31')
84
- # Period.parse_phrase('to 2015-3Q') #=> Period('2015-09-01..2015-12-31')
85
- # Period.parse_phrase('2015-3Q') #=> Period('2015-09-01..2015-12-31')
86
- #
87
- # @param phrase [String] with 'from <spec> to <spec>'
88
- # @return [Period] translated from phrase
89
- def self.parse_phrase(phrase)
90
- phrase = phrase.clean
91
- if phrase =~ /\Afrom (.*) to (.*)\z/
92
- from_phrase = $1
93
- to_phrase = $2
94
- elsif phrase =~ /\Afrom (.*)\z/
95
- from_phrase = $1
96
- to_phrase = nil
97
- elsif phrase =~ /\Ato (.*)\z/
98
- from_phrase = $1
88
+ # Period.parse_phrase('from 2014-11 to 2015-3Q') #=> [Period('2014-11-01..2015-09-30')]
89
+ # Period.parse_phrase('from 2014-11') #=> [Period('2014-11-01..2014-11-30')]
90
+ # Period.parse_phrase('from 2015-3Q') #=> [Period('2015-09-01..2015-12-31')]
91
+ # Period.parse_phrase('to 2015-3Q') #=> [Period('2015-09-01..2015-12-31')]
92
+ # Period.parse_phrase('from 2015-3Q') #=> [Period('2015-09-01..2015-12-31')]
93
+ # Period.parse_phrase('from 2015 per month') #=> [
94
+ # Period('2015-01-01..2015-01-31'),
95
+ # Period('2015-02-01..2015-02-28'),
96
+ # ...
97
+ # Period('2015-12-01..2015-12-31')
98
+ # ]
99
+ #
100
+ # @param phrase [String] with 'from <spec> to <spec> [per <chunk>]'
101
+ # @return [Array<Period>] translated from phrase
102
+ def self.parse_phrase(phrase,
103
+ partial_first: false, partial_last: false, round_up_last: false)
104
+ phrase = phrase.downcase.clean
105
+ mm = phrase.match(PHRASE_RE)
106
+ raise ArgumentError, "invalid period phrase: `#{phrase}`" unless mm
107
+
108
+ if mm[:from_part] && mm[:to_part].nil?
109
+ from_part = mm[:from_part]
110
+ to_part = nil
111
+ elsif mm[:from_part].nil? && mm[:to_part]
112
+ from_part = mm[:to_part]
113
+ to_part = nil
99
114
  else
100
- from_phrase = phrase
101
- to_phrase = nil
115
+ from_part = mm[:from_part]
116
+ to_part = mm[:to_part]
117
+ end
118
+
119
+ whole_period = parse(from_part, to_part)
120
+ if mm[:chunk_part].nil?
121
+ [whole_period]
122
+ else
123
+ whole_period.chunks(size: mm[:chunk_part], partial_first: partial_first,
124
+ partial_last: partial_last, round_up_last: round_up_last)
102
125
  end
103
- parse(from_phrase, to_phrase)
104
126
  end
105
127
 
106
128
  # @group Conversion
@@ -180,6 +202,20 @@ class Period
180
202
  !(self == other)
181
203
  end
182
204
 
205
+ # Return the hash value for this Period. Make Period's with identical
206
+ # values test eql? so that they may be used as hash keys.
207
+ #
208
+ # @return [Integer]
209
+ def hash
210
+ (first.hash | last.hash)
211
+ end
212
+
213
+ def eql?(other)
214
+ return nil unless other.is_a?(Period)
215
+
216
+ hash == other.hash
217
+ end
218
+
183
219
  # Return whether this Period contains the given date.
184
220
  #
185
221
  # @param date [Date] date to test
@@ -327,9 +363,7 @@ class Period
327
363
  raise ArgumentError, 'chunk is nil' unless chunk
328
364
 
329
365
  chunk = chunk.to_sym
330
- unless CHUNKS.include?(chunk)
331
- raise ArgumentError, "unknown chunk name: #{chunk}"
332
- end
366
+ raise ArgumentError, "unknown chunk name: #{chunk}" unless CHUNKS.include?(chunk)
333
367
 
334
368
  date = Date.ensure_date(date)
335
369
  method = "#{chunk}_containing".to_sym
@@ -456,20 +490,20 @@ class Period
456
490
  end
457
491
  end
458
492
 
459
- # Return the chunk symbol represented by the number of days given, but allow a
460
- # deviation from the minimum and maximum number of days for periods larger
461
- # than bimonths. The default tolerance is +/-10%, but that can be adjusted. The
462
- # reason for allowing a bit of tolerance for the larger periods is that
463
- # financial statements meant to cover a given calendar period are often short
464
- # or long by a few days due to such things as weekends, holidays, or
465
- # accounting convenience. For example, a bank might issuer "monthly"
466
- # statements approximately every 30 days, but issue them earlier or later to
467
- # avoid having the closing date fall on a weekend or holiday. We still want to
468
- # be able to recognize them as "monthly", even though the period covered might
469
- # be a few days shorter or longer than any possible calendar month. You can
470
- # eliminate this "fudge factor" by setting the `tolerance_pct` to zero. If
471
- # the number of days corresponds to none of the defined calendar periods,
472
- # return the symbol `:irregular`.
493
+ # Return the chunk symbol represented by the number of days given, but allow
494
+ # a deviation from the minimum and maximum number of days for periods larger
495
+ # than bimonths. The default tolerance is +/-10%, but that can be
496
+ # adjusted. The reason for allowing a bit of tolerance for the larger
497
+ # periods is that financial statements meant to cover a given calendar
498
+ # period are often short or long by a few days due to such things as
499
+ # weekends, holidays, or accounting convenience. For example, a bank might
500
+ # issuer "monthly" statements approximately every 30 days, but issue them
501
+ # earlier or later to avoid having the closing date fall on a weekend or
502
+ # holiday. We still want to be able to recognize them as "monthly", even
503
+ # though the period covered might be a few days shorter or longer than any
504
+ # possible calendar month. You can eliminate this "fudge factor" by setting
505
+ # the `tolerance_pct` to zero. If the number of days corresponds to none of
506
+ # the defined calendar periods, return the symbol `:irregular`.
473
507
  #
474
508
  # @example
475
509
  # Period.days_to_chunk(360) #=> :year
@@ -478,12 +512,12 @@ class Period
478
512
  # Period.days_to_chunk(88, 0) #=> :irregular
479
513
  #
480
514
  # @param days [Integer] the number of days in the period under test
481
- # @param tolerance_pct [Numberic] the percent deviation allowed, e.g. 10 => 10%
515
+ # @param tolerance_pct [Numeric] the percent deviation allowed, e.g. 10 => 10%
482
516
  # @return [Symbol] symbol for the period corresponding to days number of days
483
517
  def self.days_to_chunk(days, tolerance_pct = 10)
484
518
  result = :irregular
485
519
  CHUNK_RANGE.each_pair do |chunk, rng|
486
- if [:semimonth, :biweek, :week, :day].include?(chunk)
520
+ if %i[semimonth biweek week day].include?(chunk)
487
521
  # Be strict for shorter periods.
488
522
  if rng.cover?(days)
489
523
  result = chunk
@@ -574,9 +608,7 @@ class Period
574
608
  def chunks(size: :month, partial_first: false, partial_last: false,
575
609
  round_up_last: false)
576
610
  chunk_size = size.to_sym
577
- unless CHUNKS.include?(chunk_size)
578
- raise ArgumentError, "unknown chunk size '#{chunk_size}'"
579
- end
611
+ raise ArgumentError, "unknown chunk size '#{chunk_size}'" unless CHUNKS.include?(chunk_size)
580
612
 
581
613
  containing_period = Period.chunk_containing(first, chunk_size)
582
614
  return [dup] if self == containing_period
@@ -598,26 +630,25 @@ class Period
598
630
  return result
599
631
  end
600
632
 
633
+ # The first chunk
601
634
  chunk_start = first.dup
602
635
  chunk_end = chunk_start.end_of_chunk(chunk_size)
603
636
  if chunk_start.beginning_of_chunk?(chunk_size) || partial_first
604
637
  # Keep the first chunk if it's whole or partials allowed
605
638
  result << Period.new(chunk_start, chunk_end)
606
- chunk_start = chunk_end + 1.day
607
- chunk_end = chunk_start.end_of_chunk(chunk_size)
608
- else
609
- # Discard the partial first or move to next whole chunk
610
- chunk_start = chunk_end + 1.day
611
- chunk_end = chunk_start.end_of_chunk(chunk_size)
612
639
  end
640
+ chunk_start = chunk_end + 1.day
641
+ chunk_end = chunk_start.end_of_chunk(chunk_size)
642
+
613
643
  # Add Whole chunks
614
644
  while chunk_end <= last
615
645
  result << Period.new(chunk_start, chunk_end)
616
646
  chunk_start = chunk_end + 1.day
617
647
  chunk_end = chunk_start.end_of_chunk(chunk_size)
618
648
  end
649
+
619
650
  # Possibly append the final chunk to result
620
- if chunk_start < last
651
+ if chunk_start <= last
621
652
  if round_up_last
622
653
  result << Period.new(chunk_start, chunk_end)
623
654
  elsif partial_last
@@ -625,15 +656,15 @@ class Period
625
656
  else
626
657
  result
627
658
  end
628
- elsif partial_last
659
+ end
660
+ if partial_last && !partial_first && result.empty?
629
661
  # Catch the case where the period is too small to make a whole chunk and
630
662
  # partial_first is false, so it did not get included as the initial
631
663
  # partial chunk, yet a partial_last is allowed, so include the whole
632
664
  # period as a partial chunk.
633
665
  result << Period.new(first, last)
634
- else
635
- result
636
666
  end
667
+ result
637
668
  end
638
669
 
639
670
  # @group Set operations
@@ -677,7 +708,7 @@ class Period
677
708
  to_range.superset_of?(other.to_range)
678
709
  end
679
710
 
680
- # Does this period wholly contain but not coincident with `other`?
711
+ # Does this period wholly contain but is not coincident with `other`?
681
712
  #
682
713
  # @example
683
714
  # Period.parse('2015').proper_superset_of?(Period.parse('2015-2Q')) #=> true
@@ -1,3 +1,3 @@
1
1
  module FatPeriod
2
- VERSION = '1.1.1'.freeze
2
+ VERSION = '1.3.0'.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.1.1
4
+ version: 1.3.0
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-04-07 00:00:00.000000000 Z
11
+ date: 2022-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -53,21 +53,21 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry
56
+ name: debug
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: 1.0.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: 1.0.0
69
69
  - !ruby/object:Gem::Dependency
70
- name: pry-doc
70
+ name: pry
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: pry-byebug
84
+ name: pry-doc
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -108,7 +108,7 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: 4.8.3
111
- description:
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: []