fat_period 1.1.1 → 1.3.0

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