fat_period 2.0.0 → 3.0.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: 5742b6c1b70e7477cb2b8e077c3b1e5d96fc417858f618a12ad64a457df692e4
4
- data.tar.gz: 7965192d8da3ea88002089c52dc9344cae1f75e7fab28739185b5f290c3f1bc9
3
+ metadata.gz: dadf65165f7640c1ed71d1b39446a330f810779a23acc6975986249b71918a8e
4
+ data.tar.gz: c118e93977b2133c99fbb53bcc64eea6dd7a98f30f05bfe64742f501d7d60753
5
5
  SHA512:
6
- metadata.gz: 78bf26849daf793f3b2115e136599178817fd43a1a03bc2882eea408bd7995c44feb94998f22bb220afeb3c2b8270770aa7dc586736849af5f5506f6f7e1123b
7
- data.tar.gz: dc0b08cb99a577a8fe11827e96ef1c9473a4536fe4ae0e7dd483fc7a1d1d0493a333eecbbcc8a2cd9f710d03489bc32aa4c8695b9787007dc51efddb9805e14b
6
+ metadata.gz: 8cb63a7e83eb0035be2d3757a7969ff5c9ef244be16495e4aa0a1dcb2276e229d287a695ce8ec8cfdff21c891c076a2d29b6ccbffdb585f821fcfe578e8f3dde
7
+ data.tar.gz: 4d121f6f04a7780ded6ed2d6f7bb8de6c32e89ea2e96d97f14029f1800ce6ca4516dd6769887210ca5557816c978c39b270b289dee1f84d61737d6216e4ae4d5
data/.envrc CHANGED
@@ -1,5 +1,4 @@
1
1
  PATH_add .bundle/bin
2
- PATH_add ./scripts
3
2
  # Ensure that we use the development DB when in the src dir.
4
3
  export DB=development
5
4
  export COMPOSE_FILE=.devcontainer/docker-compose.yml
@@ -0,0 +1,54 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby CI (setup-ruby)
9
+
10
+ on:
11
+ push:
12
+ pull_request:
13
+
14
+ jobs:
15
+ test:
16
+ runs-on: ubuntu-24.04
17
+ strategy:
18
+ matrix:
19
+ ruby: [3.2.2, 3.3.0, 3.4.0]
20
+ steps:
21
+ - name: Check out code
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Install build dependencies (needed if Ruby must be compiled)
25
+ run: |
26
+ sudo apt-get update
27
+ sudo apt-get install -y --no-install-recommends \
28
+ build-essential autoconf bison libssl-dev libreadline-dev \
29
+ zlib1g-dev libyaml-dev libffi-dev libgdbm-dev libncurses5-dev \
30
+ libgmp-dev pkg-config
31
+
32
+ - name: Set up Ruby
33
+ uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: ${{ matrix.ruby }}
36
+ bundler-cache: true
37
+
38
+ - name: Verify Ruby
39
+ run: |
40
+ ruby -v
41
+ gem -v
42
+ shell: bash
43
+
44
+ - name: Install gems
45
+ run: |
46
+ gem install bundler --conservative
47
+ # optional: use vendor/bundle to keep gems within repo workspace
48
+ bundle config set --local path 'vendor/bundle' || true
49
+ bundle install --jobs 4 --retry 3
50
+ shell: bash
51
+
52
+ - name: Run tests
53
+ run: bundle exec rake
54
+ shell: bash
data/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ # Inherit the shared config from the rubocop-ddoherty gem (git-hosted on GitHub)
2
+ inherit_gem:
3
+ rubocop-ddoherty: 'config/default.yml'
data/CHANGELOG.org ADDED
@@ -0,0 +1,29 @@
1
+ * Version 3.0.0
2
+ - Use the newly-spun-out fat_date library in place of fat_core/date, which no
3
+ longer exists starting with fat_core version 6.0.0.
4
+ - The fat_date library uses ~Date.spec~ instead of ~Date.parse_spec~, which is
5
+ just cleaner to type.
6
+ - The new ~fat_date~ libraries come with some notable enhancements to date
7
+ specs, including:
8
+ 1. Skip modifiers, which allow the spec to "skip" from its usual target date
9
+ to the nearest day-of-week, either before or after the target date, and
10
+ either including or not including the target date as a candidate.
11
+ 2. Changes the semimonth specifier from uppercase roman to A or B, so
12
+ 2025-10-A (rather than 2025-I) is now the first semimonth for October,
13
+ 2025, and 2025-10-B (rather than 2025-II) is now the second semimonth.
14
+ This allows week-of-month specs to be case insensitive, so 2025-10-i,
15
+ 2025-10-ii, 2025-10-iii, 2025-10-iv, and 2025-10-v, can represent the
16
+ first through fifth week of October, 2025, as well as 2025-10-I,
17
+ 2025-10-II, 2025-10-III, 2025-10-IV, and 2025-10-V.
18
+ 3. In general, all the spec involving letters should now be case insensitive.
19
+ - Some other code cleanup with no impact on the API.
20
+
21
+ * Version 2.1.0
22
+ - When the first date is later than the second in ~Period.new~, raise
23
+ ~ArgumentError~ exception instead of returning nil.
24
+ - Allow ~Period.parse_phrase~ to parse a 'per' clause and return an Array of
25
+ Periods that are sub-periods of the period given by the from- and
26
+ to-clauses.
27
+ - Add ~Period.ensure(arg)~ to try to return a ~Period~ from a string that can
28
+ be parsed as a ~Period~, an object that has a ~to_period~ method, or return
29
+ the ~arg~ if its already a ~Period~.
data/Gemfile CHANGED
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in fat_period.gemspec
4
6
  gemspec
5
7
 
6
- gem 'bundler'
7
- gem 'debug'
8
- gem 'pry'
9
- gem 'pry-doc'
10
- gem 'rake'
11
- gem 'rspec'
12
- gem 'rubocop'
13
- gem 'rubocop-rspec'
14
- gem 'rubocop-performance'
15
- gem 'rubocop-rake'
16
- gem 'rubocop-shopify'
8
+ group :development do
9
+ gem 'bundler'
10
+ gem 'debug'
11
+ gem 'pry'
12
+ gem 'pry-doc'
13
+ gem 'rake'
14
+ gem 'rspec'
15
+ gem 'rubocop', require: false
16
+ gem 'rubocop-ddoherty', git: 'https://github.com/ddoherty03/rubocop-ddoherty.git', branch: 'master', require: false
17
+ end
data/README.org CHANGED
@@ -1,22 +1,103 @@
1
- #+OPTIONS: :toc
1
+ #+TITLE: FatPeriod User Guide
2
+ #+OPTIONS: toc:5
3
+ #+PROPERTY: header-args:ruby :colnames no :session fat_period :hlines yes :exports both :wrap example :ruby "bundle exec irb --prompt=simple"
4
+ #+PROPERTY: header-args:sh :exports code
2
5
  #+LATEX_HEADER: \usepackage[margin=0.75in]{geometry}
3
6
 
4
- #+BEGIN_COMMENT
5
- This is for markdown output:
7
+ [[https://github.com/ddoherty03/fat_period/actions/workflows/ruby.yml][https://github.com/ddoherty03/fat_period/actions/workflows/ruby.yml/badge.svg?branch=master]]
6
8
 
7
- The following is for org.
8
- #+END_COMMENT
9
+ * Setup for Code Blocks :noexport:
10
+ Run this block before all others to ensure that we are reading the libraries
11
+ from the source directory.
9
12
 
10
- [[https://travis-ci.org/ddoherty03/fat_table.svg?branch=master]]
13
+ #+begin_src ruby :results output :export no
14
+ Dir.chdir("/home/ded/src/fat_period")
15
+ puts "Current directory: #{Dir.pwd}"
16
+ puts "Ruby LOADPATH:"
17
+ $LOAD_PATH.unshift(File.expand_path("lib", Dir.pwd)) unless $:[0].match?(%r{src/fat_period/lib})
18
+ $:[0..10].each { |d| puts d }
19
+ puts "..."
20
+ require 'fat_period'
21
+ #+end_src
22
+
23
+ #+RESULTS:
24
+ #+begin_example
25
+ Current directory: /home/ded/src/fat_period
26
+ Ruby LOADPATH:
27
+ /home/ded/src/fat_period/lib
28
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/bundler-2.6.7/lib
29
+ /home/ded/.rbenv/rbenv.d/exec/gem-rehash
30
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/bundler/gems/rubocop-ddoherty-6b28e9614f18/lib
31
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-shopify-2.17.1/lib
32
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-rspec-3.7.0/lib
33
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-rake-0.7.1/lib
34
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-performance-1.26.1/lib
35
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/rubocop-1.81.6/lib
36
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/unicode-display_width-3.2.0/lib
37
+ /home/ded/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/unicode-emoji-4.1.0/lib
38
+ ...
39
+ #+end_example
40
+
41
+ * Version
42
+ #+begin_src ruby :wrap EXAMPLE
43
+ "Current version is: #{FatPeriod::VERSION}"
44
+ #+end_src
45
+
46
+ #+begin_EXAMPLE
47
+ Current version is: 3.0.0
48
+ #+end_EXAMPLE
11
49
 
12
50
  * Introduction
13
51
 
14
- ~FatPeriod~ provides a Ruby ~Period~ class for dealing with time periods, that
15
- is ranges whose endpoints are ~Date~ s. Set operations, for example, are
16
- provided for Period, as well as methods for parsing strings into Periods and
17
- methods for breaking a larger periods into an array of smaller periods of
18
- various 'chunk' sizes that correspond to calendar-related periods such as
19
- days, weeks, months, and so forth.'
52
+ ~FatPeriod~ provides a Ruby ~Period~ class for dealing with time periods of
53
+ days, that is ranges whose endpoints are of class ~Date~. It's target is
54
+ financial applications, but it serves well for any application where periods
55
+ of time are useful. It builds on the [[https://github.com/ddoherty03/fat_date][fat_date]] gem, which provides
56
+ enhancements to the ~Date~ class, especially its class method ~Date.spec~ for
57
+ interpreting a rich set of "specs" as the beginning or end of a variety of
58
+ calendar-related periods.
59
+
60
+ In addition, set operations are provided for Period, as well as methods for
61
+ breaking a larger periods into an array of smaller periods of various 'chunk'
62
+ sizes that correspond to calendar-related periods such as days, weeks, months,
63
+ and so forth.
64
+
65
+ * Table of Contents :toc:noexport:
66
+ - [[#version][Version]]
67
+ - [[#introduction][Introduction]]
68
+ - [[#installation][Installation]]
69
+ - [[#installing-the-gem][Installing the gem]]
70
+ - [[#usage][Usage]]
71
+ - [[#constant-periodforever][Constant Period::FOREVER]]
72
+ - [[#construction-of-periods][Construction of Periods]]
73
+ - [[#periodnew][Period.new]]
74
+ - [[#periodparse][Period.parse]]
75
+ - [[#with-only-a-from-spec][With Only a From-Spec]]
76
+ - [[#with-both-a-from-spec-and-to-spec][With Both a From-Spec and To-Spec]]
77
+ - [[#using-skip-modifiers][Using Skip Modifiers]]
78
+ - [[#periodparse_phrase][Period.parse_phrase]]
79
+ - [[#periodensure][Period.ensure]]
80
+ - [[#conversion][Conversion]]
81
+ - [[#to-range][To Range]]
82
+ - [[#to-string][To String]]
83
+ - [[#tex-form][TeX Form]]
84
+ - [[#comparison][Comparison]]
85
+ - [[#enumeration][Enumeration]]
86
+ - [[#size][Size]]
87
+ - [[#chunking][Chunking]]
88
+ - [[#set-operations][Set Operations]]
89
+ - [[#subset-determination][Subset Determination]]
90
+ - [[#superset-determination][Superset Determination]]
91
+ - [[#intersection][Intersection]]
92
+ - [[#difference][Difference]]
93
+ - [[#union][Union]]
94
+ - [[#coverage][Coverage]]
95
+ - [[#contains][Contains?]]
96
+ - [[#overlapping][Overlapping]]
97
+ - [[#spanning][Spanning]]
98
+ - [[#gaps][Gaps]]
99
+ - [[#development][Development]]
100
+ - [[#contributing][Contributing]]
20
101
 
21
102
  * Installation
22
103
 
@@ -41,9 +122,17 @@ Or install it yourself as:
41
122
  #+END_SRC
42
123
 
43
124
  * Usage
125
+ ** Constant Period::FOREVER
44
126
 
45
- ** Construction of Periods
127
+ The ~Period~ class depends on the extensions to ~Date~ made by the ~fat_core~
128
+ gem, which you can read about [[https://github.com/ddoherty03/fat_core][here]]. It defines a constant, ~Period::FOREVER~,
129
+ which is defined as extending from ~Date::BOT~ to ~Date::EOT~, which are
130
+ defined in ~fat_date~ as 1900-01-01 and 3000-12-31, respectively and define
131
+ the beginning of time and end of time for practical commercial purposes. The
132
+ constant is not frozen, so you can re-define it to your liking.
46
133
 
134
+ ** Construction of Periods
135
+ *** Period.new
47
136
  A Period is constructed with two arguments for the begin and end date. The
48
137
  begin date must be on or before the end date. Each argument can be (1) a
49
138
  Date, (2) a string parseable as a Date by the Date.parse method, or (3) an
@@ -53,15 +142,295 @@ object that responds to ~#to_s~ and can be parsed as a Date by Date.parse:
53
142
  p1 = Period.new(Date.today, Date.today + 30)
54
143
  p2 = Period.new('Nov 22, 1963', Date.today)
55
144
  p3 = Period.new('1961-01-21', '1963-11-22')
56
- puts "Camelot lasted #{p3.length} days"
145
+ [[p1.to_s], [p2.to_s], [p3.to_s]]
57
146
  #+end_SRC
58
147
 
59
- ** Period Constants
148
+ #+RESULTS:
149
+ | 2025-03-20 to 2025-04-19 |
150
+ | 1963-11-22 to 2025-03-20 |
151
+ | 1961-01-21 to 1963-11-22 |
60
152
 
61
- The ~Period~ class depends on the extensions to ~Date~ made by the ~fat_core~
62
- gem, which you can read about [[https://github.com/ddoherty03/fat_core][here]]. It defines two constants, ~Date::BOT~ and
63
- ~Date::EOT~, which define beginning of time and end of time for practical
64
- commercial purposes.
153
+ #+begin_src ruby
154
+ ["Camelot lasted #{p3.length} days"]
155
+ #+end_src
156
+
157
+ #+RESULTS:
158
+ | Camelot lasted 1036 days |
159
+
160
+ *** Period.parse
161
+ A more convenient way to construct a period is provided by ~Period.parse~. It
162
+ takes two strings as its arguments, a mandatory "from-spec" and an optional
163
+ "to-spec":
164
+
165
+ A "spec" is a string designating some period of time. There are many ways of
166
+ specifying a period, which are detailed below.
167
+
168
+ **** With Only a From-Spec
169
+
170
+ If only a from-spec is given, it defines both the beginning and end of the
171
+ overall period:
172
+
173
+ #+begin_src ruby
174
+ tab = []
175
+ tab << ['From Spec', 'Result']
176
+ tab << nil
177
+ froms = ['2020', '2020-2Q', '2020-W15', '2020-09', '2020-09-A', '2020-09-iii']
178
+ froms.each do |f|
179
+ tab << [f, Period.parse(f).inspect]
180
+ end
181
+ tab
182
+ #+end_src
183
+
184
+ #+begin_example
185
+ | From Spec | Result |
186
+ |-------------+--------------------------------|
187
+ | 2020 | Period(2020-01-01..2020-12-31) |
188
+ | 2020-2Q | Period(2020-04-01..2020-06-30) |
189
+ | 2020-W15 | Period(2020-04-06..2020-04-12) |
190
+ | 2020-09 | Period(2020-09-01..2020-09-30) |
191
+ | 2020-09-A | Period(2020-09-01..2020-09-15) |
192
+ | 2020-09-iii | Period(2020-09-14..2020-09-20) |
193
+ #+end_example
194
+
195
+ **** With Both a From-Spec and To-Spec
196
+ But, if a to-spec is also given, the from-spec defines the beginning of the
197
+ period and the to-spec defines the end of the period. In particular, the
198
+ beginning of the period is the first day of the from-spec and the end of the
199
+ period is the last day of the to-spec:
200
+
201
+ #+begin_src ruby
202
+ tab = []
203
+ tab << ['From Spec', 'To Spec', 'Result']
204
+ tab << nil
205
+ from_tos = [['2020', '2020-2Q'], ['2020-2Q', '2020-W15'], ['2020-W15', '2020-09'], ['2020-09', '2020-09-A'], ['2020-09-A', '2020-09-iii']]
206
+ from_tos.each do |f, t|
207
+ tab << [f, t, Period.parse(f, t).inspect]
208
+ end
209
+ tab
210
+ #+end_src
211
+
212
+ #+begin_example
213
+ | From Spec | To Spec | Result |
214
+ |-----------+-------------+--------------------------------|
215
+ | 2020 | 2020-2Q | Period(2020-01-01..2020-06-30) |
216
+ | 2020-2Q | 2020-W15 | Period(2020-04-01..2020-04-12) |
217
+ | 2020-W15 | 2020-09 | Period(2020-04-06..2020-09-30) |
218
+ | 2020-09 | 2020-09-A | Period(2020-09-01..2020-09-15) |
219
+ | 2020-09-A | 2020-09-iii | Period(2020-09-01..2020-09-20) |
220
+ #+end_example
221
+
222
+ **** Using Skip Modifiers
223
+ One new feature of FatDate is the ability to add a "skip modifier" to the end
224
+ of a date spec to skip forward or backward to the first day-of-week either on
225
+ or before/after the date given by the spec. For example, the following
226
+ demonstrates that one can set the 'to' spec to the /last/ Wednesday of 2025 or
227
+ the last Wednesday /before/ the end of 2025. Using '>' or '>=' specified
228
+ skipping forward instead.
229
+
230
+ #+begin_src ruby
231
+ tab = []
232
+ tab << ['From Spec', 'To Spec', 'Result', 'Description']
233
+ tab << nil
234
+ from_to_descs = [['2025-2Q', '2025<=Wed', 'From 2q to last Wednesday of 2025'],
235
+ ['2025-2Q', '2025<Wed', 'From 2q to last Wednesday /before/ the end of 2025'],
236
+ ['2012-11', '2012-11<=Thur', 'November 2012 through last Thursday'],
237
+ ['2012-11', '2012-11-4Thur', 'And through Thanksgiving (not always the /last/ Thursday!)']
238
+ ]
239
+ from_to_descs.each do |f, t, d|
240
+ tab << [f, t, Period.parse(f, t).inspect, d]
241
+ end
242
+ tab
243
+ #+end_src
244
+
245
+ #+begin_example
246
+ | From Spec | To Spec | Result | Description |
247
+ |-----------+---------------+--------------------------------+------------------------------------------------------------|
248
+ | 2025-2Q | 2025<=Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday of 2025 |
249
+ | 2025-2Q | 2025<Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday /before/ the end of 2025 |
250
+ | 2012-11 | 2012-11<=Thur | Period(2012-11-01..2012-11-29) | November 2012 through last Thursday |
251
+ | 2012-11 | 2012-11-4Thur | Period(2012-11-01..2012-11-22) | And through Thanksgiving (not always the /last/ Thursday!) |
252
+ #+end_example
253
+
254
+ *** Period.parse_phrase
255
+ For example:
256
+
257
+ The ~Period.parse_phrase~ method will take a string having a 'from', 'to', and
258
+ 'per' clause and return an Array of Periods encompassing the same period as
259
+ ~Period.parse~, but optionally broken into sub-periods each having the length
260
+ specified by the 'per' clause. ~Period.parse_phrase~ always returns an Array
261
+ of Periods even if there is no 'per' clause and the Array has only one
262
+ member. If there is no 'to' clause, the returned period is from the start of
263
+ the 'from' period to its end. If there is neither a 'from' or a 'to' clause,
264
+ it tries to interpret the beginning of the phrase as a valid spec and uses it
265
+ as a 'from' clause.
266
+
267
+ #+begin_src ruby
268
+ tab = []
269
+ tab << ['k', 'Sub Period']
270
+ tab << nil
271
+ pds = Period.parse_phrase('from 2025 to 2025-3Q per month')
272
+ pds.each_with_index do |pd, k|
273
+ tab << [k, pd.inspect]
274
+ end
275
+ tab
276
+ #+end_src
277
+
278
+ #+begin_example
279
+ | k | Sub Period |
280
+ |---+--------------------------------|
281
+ | 0 | Period(2025-01-01..2025-01-31) |
282
+ | 1 | Period(2025-02-01..2025-02-28) |
283
+ | 2 | Period(2025-03-01..2025-03-31) |
284
+ | 3 | Period(2025-04-01..2025-04-30) |
285
+ | 4 | Period(2025-05-01..2025-05-31) |
286
+ | 5 | Period(2025-06-01..2025-06-30) |
287
+ | 6 | Period(2025-07-01..2025-07-31) |
288
+ | 7 | Period(2025-08-01..2025-08-31) |
289
+ | 8 | Period(2025-09-01..2025-09-30) |
290
+ #+end_example
291
+
292
+ The period named in the 'per' clause is called a 'chunk' and there are several
293
+ valid chunk names in ~FatPeriod~:
294
+
295
+ | Chunk Name |
296
+ |------------|
297
+ | year |
298
+ | half |
299
+ | quarter |
300
+ | bimonth |
301
+ | month |
302
+ | semimonth |
303
+ | biweek |
304
+ | week |
305
+ | day |
306
+
307
+ Here is the same period broken into weeks. Notice that the first and last
308
+ "weeks" are not whole weeks because parts of them fall outside the boundaries
309
+ of the overall period.
310
+
311
+ #+begin_src ruby
312
+ tab = []
313
+ tab << ['k', 'Sub Period']
314
+ tab << nil
315
+ pds = Period.parse_phrase('from 2025 to 2025-3Q per week')
316
+ pds.each_with_index do |pd, k|
317
+ tab << [k, pd.inspect]
318
+ end
319
+ tab
320
+ #+end_src
321
+
322
+ #+begin_example
323
+ | k | Sub Period |
324
+ |----+--------------------------------|
325
+ | 0 | Period(2025-01-01..2025-01-05) |
326
+ | 1 | Period(2025-01-06..2025-01-12) |
327
+ | 2 | Period(2025-01-13..2025-01-19) |
328
+ | 3 | Period(2025-01-20..2025-01-26) |
329
+ | 4 | Period(2025-01-27..2025-02-02) |
330
+ | 5 | Period(2025-02-03..2025-02-09) |
331
+ | 6 | Period(2025-02-10..2025-02-16) |
332
+ | 7 | Period(2025-02-17..2025-02-23) |
333
+ | 8 | Period(2025-02-24..2025-03-02) |
334
+ | 9 | Period(2025-03-03..2025-03-09) |
335
+ | 10 | Period(2025-03-10..2025-03-16) |
336
+ | 11 | Period(2025-03-17..2025-03-23) |
337
+ | 12 | Period(2025-03-24..2025-03-30) |
338
+ | 13 | Period(2025-03-31..2025-04-06) |
339
+ | 14 | Period(2025-04-07..2025-04-13) |
340
+ | 15 | Period(2025-04-14..2025-04-20) |
341
+ | 16 | Period(2025-04-21..2025-04-27) |
342
+ | 17 | Period(2025-04-28..2025-05-04) |
343
+ | 18 | Period(2025-05-05..2025-05-11) |
344
+ | 19 | Period(2025-05-12..2025-05-18) |
345
+ | 20 | Period(2025-05-19..2025-05-25) |
346
+ | 21 | Period(2025-05-26..2025-06-01) |
347
+ | 22 | Period(2025-06-02..2025-06-08) |
348
+ | 23 | Period(2025-06-09..2025-06-15) |
349
+ | 24 | Period(2025-06-16..2025-06-22) |
350
+ | 25 | Period(2025-06-23..2025-06-29) |
351
+ | 26 | Period(2025-06-30..2025-07-06) |
352
+ | 27 | Period(2025-07-07..2025-07-13) |
353
+ | 28 | Period(2025-07-14..2025-07-20) |
354
+ | 29 | Period(2025-07-21..2025-07-27) |
355
+ | 30 | Period(2025-07-28..2025-08-03) |
356
+ | 31 | Period(2025-08-04..2025-08-10) |
357
+ | 32 | Period(2025-08-11..2025-08-17) |
358
+ | 33 | Period(2025-08-18..2025-08-24) |
359
+ | 34 | Period(2025-08-25..2025-08-31) |
360
+ | 35 | Period(2025-09-01..2025-09-07) |
361
+ | 36 | Period(2025-09-08..2025-09-14) |
362
+ | 37 | Period(2025-09-15..2025-09-21) |
363
+ | 38 | Period(2025-09-22..2025-09-28) |
364
+ | 39 | Period(2025-09-29..2025-09-30) |
365
+ #+end_example
366
+
367
+
368
+ *** Period.ensure
369
+ ~Period.ensure~ tries to interpret its argument as a ~Period~ and returns it;
370
+ otherwise it throws an ~ArgumentError~ exception:
371
+
372
+ - if the argument responds to the ~to_period~ method, it invokes that on the
373
+ argument and returns it;
374
+ - if the argument is a ~String~, it uses ~Period.parse_phrase~ to try to
375
+ interepret it as a ~Period~;
376
+ - if it is already a ~Period~, it just returns the argument;
377
+ - otherwise, it throws an ~ArgumentError~ exception.
378
+
379
+
380
+ #+begin_src ruby
381
+ class ContainingMonth
382
+ def initialize(dat)
383
+ @dat = Date.ensure(dat)
384
+ end
385
+
386
+ def to_period
387
+ Period.month_containing(@dat)
388
+ end
389
+ end
390
+
391
+ cm = ContainingMonth.new('2025-09-22')
392
+ Period.ensure(cm)
393
+ #+end_src
394
+
395
+ #+begin_example
396
+ Period(2025-09-01..2025-09-30)
397
+ #+end_example
398
+
399
+ #+begin_src ruby
400
+ Period.ensure('from 2016 to 2018-3Q')
401
+ #+end_src
402
+
403
+ #+begin_example
404
+ Period(2016-01-01..2018-09-30)
405
+ #+end_example
406
+
407
+ #+begin_src ruby
408
+ Period.ensure(Period.new('2016-01-02', '2017-09-29'))
409
+ #+end_src
410
+
411
+ #+begin_example
412
+ Period(2016-01-02..2017-09-29)
413
+ #+end_example
414
+
415
+ ** Conversion
416
+ *** To Range
417
+ *** To String
418
+ *** TeX Form
419
+ ** Comparison
420
+ ** Enumeration
421
+ ** Size
422
+ ** Chunking
423
+ ** Set Operations
424
+ *** Subset Determination
425
+ *** Superset Determination
426
+ *** Intersection
427
+ *** Difference
428
+ *** Union
429
+ ** Coverage
430
+ *** Contains?
431
+ *** Overlapping
432
+ *** Spanning
433
+ *** Gaps
65
434
 
66
435
  * Development
67
436
 
data/Rakefile CHANGED
@@ -1,9 +1,26 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
3
2
 
4
- require 'rubocop/rake_task'
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
- RuboCop::RakeTask.new
7
6
  RSpec::Core::RakeTask.new(:spec)
8
7
 
9
- task :default => [:spec, :rubocop]
8
+ ########################################################################
9
+ # Rubocop tasks
10
+ ########################################################################
11
+ # Option A (recommended): Keep using Bundler and run rubocop via `bundle exec`.
12
+ # This wrapper task ensures the rubocop run uses the gems from your Gemfile,
13
+ # even when you invoke `rake rubocop` (no need to remember `bundle exec rake`).
14
+ #
15
+ # You can pass extra RuboCop CLI flags with the RUBOCOP_OPTS environment variable:
16
+ # RUBOCOP_OPTS="--format simple" rake rubocop
17
+
18
+ desc 'Run rubocop under `bundle exec`'
19
+ task :rubocop do
20
+ opts = (ENV['RUBOCOP_OPTS'] || '').split
21
+ Bundler.with_unbundled_env do
22
+ sh 'bundle', 'exec', 'rubocop', *opts
23
+ end
24
+ end
25
+
26
+ task default: %i[spec rubocop]
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'fat_period'
data/fat_period.gemspec CHANGED
@@ -1,6 +1,6 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'fat_period/version'
6
6
 
@@ -21,5 +21,6 @@ Gem::Specification.new do |spec|
21
21
  # spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ['lib']
23
23
 
24
- spec.add_dependency 'fat_core', '>= 5.4'
24
+ spec.add_dependency 'fat_core', '>= 6.0'
25
+ spec.add_dependency 'fat_date'
25
26
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FatPeriod
2
4
  # An extension of Date for methods useful with respect to FatPeriod::Periods.
3
5
  module Date
@@ -1,5 +1,6 @@
1
- require 'active_support'
2
- require 'fat_core/date'
1
+ # frozen_string_literal: true
2
+
3
+ require 'fat_date'
3
4
 
4
5
  # The Period class represents a range of Dates and supports a variety of
5
6
  # operations on those ranges.
@@ -30,9 +31,7 @@ class Period
30
31
  @last = Date.ensure_date(last).freeze
31
32
  freeze
32
33
 
33
- return unless @first > @last
34
-
35
- raise ArgumentError, "Period's first date is later than its last date"
34
+ raise ArgumentError, "Period's first date is later than its last date" if @first > @last
36
35
  end
37
36
 
38
37
  # These need to come after initialize is defined
@@ -43,7 +42,7 @@ class Period
43
42
  # @group Parsing
44
43
  #
45
44
  # Return a period based on two date specs passed as strings (see
46
- # `FatCore::Date.parse_spec`), a 'from' and a 'to' spec. The returned period
45
+ # `FatCore::Date.spec`), a 'from' and a 'to' spec. The returned period
47
46
  # begins on the first day of the period given as the `from` spec and ends on
48
47
  # the last day given as the `to` spec. If the to spec is not given or is nil,
49
48
  # the from spec is used for both the from- and to-spec.
@@ -54,47 +53,86 @@ class Period
54
53
  # # Assuming this executes in December, 2014
55
54
  # Period.parse('last_month', 'this_month').inspect #=> Period('2014-11-01..2014-12-31')
56
55
  #
57
- # @param from [String] spec ala FatCore::Date.parse_spec
58
- # @param to [String] spec ala FatCore::Date.parse_spec
56
+ # @param from [String] spec ala FatCore::Date.spec
57
+ # @param to [String] spec ala FatCore::Date.spec
59
58
  # @return [Period] from beginning of `from` to end of `to`
60
59
  def self.parse(from, to = nil)
61
60
  raise ArgumentError, 'Period.parse missing argument' unless from
62
61
 
63
62
  to ||= from
64
- first = Date.parse_spec(from, :from)
65
- second = Date.parse_spec(to, :to)
63
+ first = Date.spec(from, :from)
64
+ second = Date.spec(to, :to)
66
65
  Period.new(first, second) if first && second
67
66
  end
68
67
 
69
- # Return a period as in `Period.parse` from a String phrase in which the from
70
- # spec is introduced with 'from' and, optionally, the to spec is introduced
71
- # with 'to'. A phrase with only a to spec is treated the same as one with
72
- # only a from spec. If neither 'from' nor 'to' appear in phrase, treat the
73
- # whole string as a from spec.
68
+ # Return a Period either from a given String or other type that can
69
+ # reasonably converted to a Period.
74
70
  #
75
71
  # @example
76
- # Period.parse_phrase('from 2014-11 to 2015-3Q') #=> Period('2014-11-01..2015-09-30')
77
- # Period.parse_phrase('from 2014-11') #=> Period('2014-11-01..2014-11-30')
78
- # Period.parse_phrase('from 2015-3Q') #=> Period('2015-09-01..2015-12-31')
79
- # Period.parse_phrase('to 2015-3Q') #=> Period('2015-09-01..2015-12-31')
80
- # Period.parse_phrase('2015-3Q') #=> Period('2015-09-01..2015-12-31')
72
+ # Period.ensure('2014-11').inspect #=> Period('2014-11-01..2014-11-30')
73
+ # pd = Period.parse('2011')
74
+ # Period.ensure(pd).inspect #=> Period('2011-01-01..2011-12-31')
75
+ #
76
+ # @param prd [String|Period] or any candidate for conversion to Period
77
+ # @return Period correspondign to prd parameter
78
+ def self.ensure(prd)
79
+ return prd.to_period if prd.respond_to?(:to_period)
80
+
81
+ case prd
82
+ when String
83
+ if prd.match?(/from|to/i)
84
+ Period.parse_phrase(prd).first
85
+ else
86
+ Period.parse(prd)
87
+ end
88
+ when Period
89
+ prd
90
+ end
91
+ end
92
+
93
+ # Return an Array of Periods from a String phrase in which the from spec is
94
+ # introduced with 'from' and, optionally, the to spec is introduced with
95
+ # 'to' and optionally a 'per' clause is introduced by 'per'. A phrase with
96
+ # only a to spec is treated the same as one with only a from spec. If
97
+ # neither 'from' nor 'to' appear in phrase, treat the string before any
98
+ # per-clause as a from spec.
81
99
  #
82
- # @param phrase [String] with 'from <spec> to <spec>'
100
+ # @example
101
+ # Period.parse_phrase('from 2014-11 to 2015-3Q') #=> [Period('2014-11-01..2015-09-30')]
102
+ # Period.parse_phrase('from 2014-11') #=> [Period('2014-11-01..2014-11-30')]
103
+ # Period.parse_phrase('from 2015-3Q') #=> [Period('2015-09-01..2015-12-31')]
104
+ # Period.parse_phrase('to 2015-3Q') #=> [Period('2015-09-01..2015-12-31')]
105
+ # Period.parse_phrase('2015-3Q') #=> [Period('2015-09-01..2015-12-31')]
106
+ # Period.parse_phrase('to 2015-3Q per week') #=> [Period('2015-09-01..2015-09-04')...]
107
+ # Period.parse_phrase('2015-3Q per month') #=> [Period('2015-09-01..2015-09-30')...]
108
+ #
109
+ # @param phrase [String] with 'from <spec> [to <spec>] [per chunk]'
83
110
  # @return [Period] translated from phrase
84
- def self.parse_phrase(phrase)
111
+ def self.parse_phrase(phrase, partial_first: true, partial_last: true, round_up_last: false)
85
112
  phrase = phrase.clean
86
113
  case phrase
87
- when /\Afrom (.*) to (.*)\z/
88
- from_phrase = $1
89
- to_phrase = $2
90
- when /\Afrom (.*)\z/, /\Ato (.*)\z/
91
- from_phrase = $1
114
+ when /\Afrom\s+([^\s]+)\s+to\s+([^\s]+)(\s+per\s+[^\s]+)?\z/i
115
+ from_phrase = ::Regexp.last_match(1)
116
+ to_phrase = ::Regexp.last_match(2)
117
+ when /\Afrom\s+([^\s]+)(\s+per\s+[^\s]+)?\z/, /\Ato\s+([^\s]+)(\s+per\s+[^\s]+)?\z/i
118
+ from_phrase = ::Regexp.last_match(1)
92
119
  to_phrase = nil
93
- else
94
- from_phrase = phrase
120
+ when /\A([^\s]+)(\s+per\s+[^\s]+)?\z/
121
+ from_phrase = ::Regexp.last_match(1)
95
122
  to_phrase = nil
123
+ else
124
+ raise ArgumentError, "unintelligible period phrase: '#{phrase}''"
125
+ end
126
+ # Return an Array of periods divided by chunks if any.
127
+ whole_period = parse(from_phrase, to_phrase)
128
+ if phrase =~ /per\s+(?<chunk>[a-z_]+)/i
129
+ chunk_size = Regexp.last_match[:chunk].downcase.to_sym
130
+ raise ArgumentError, "invalid chunk size #{chunk_size}" unless CHUNKS.include?(chunk_size)
131
+
132
+ whole_period.chunks(size: chunk_size, partial_first:, partial_last:, round_up_last:)
133
+ else
134
+ [whole_period]
96
135
  end
97
- parse(from_phrase, to_phrase)
98
136
  end
99
137
 
100
138
  # @group Conversion
@@ -183,23 +221,11 @@ class Period
183
221
  end
184
222
 
185
223
  def eql?(other)
186
- return unless other.is_a?(Period)
224
+ return false unless other.is_a?(Period)
187
225
 
188
226
  hash == other.hash
189
227
  end
190
228
 
191
- # Return whether this Period contains the given date.
192
- #
193
- # @param date [Date] date to test
194
- # @return [Boolean] is the given date within this Period?
195
- def contains?(date)
196
- date = date.to_date if date.respond_to?(:to_date)
197
- raise ArgumentError, 'argument must be a Date' unless date.is_a?(Date)
198
-
199
- to_range.cover?(date)
200
- end
201
- alias_method :===, :contains?
202
-
203
229
  include Enumerable
204
230
 
205
231
  # @group Enumeration
@@ -284,7 +310,6 @@ class Period
284
310
  quarter
285
311
  half
286
312
  year
287
- irregular
288
313
  ].freeze
289
314
 
290
315
  CHUNK_ORDER = {}
@@ -784,6 +809,18 @@ class Period
784
809
  to_range.overlaps?(other.to_range)
785
810
  end
786
811
 
812
+ # Return whether this Period contains the given date.
813
+ #
814
+ # @param date [Date] date to test
815
+ # @return [Boolean] is the given date within this Period?
816
+ def contains?(date)
817
+ date = date.to_date if date.respond_to?(:to_date)
818
+ raise ArgumentError, 'argument must be a Date' unless date.is_a?(Date)
819
+
820
+ to_range.cover?(date)
821
+ end
822
+ alias_method :===, :contains?
823
+
787
824
  # Return whether any of the given periods overlap any other.
788
825
  #
789
826
  # @example
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FatPeriod
2
- VERSION = '2.0.0'.freeze
4
+ VERSION = '3.0.0'
3
5
  end
data/lib/fat_period.rb CHANGED
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
 
5
+ require 'active_support'
6
+
3
7
  require 'fat_period/version'
4
8
  require 'fat_period/date'
5
9
  require 'fat_period/period'
6
10
 
7
- require 'fat_core/date'
8
11
  require 'fat_core/range'
9
12
  require 'fat_core/string'
13
+
14
+ require 'fat_date'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fat_period
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel E. Doherty
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-11-12 00:00:00.000000000 Z
10
+ date: 2025-10-28 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: fat_core
@@ -16,15 +15,28 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '5.4'
18
+ version: '6.0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '5.4'
27
- description:
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: fat_date
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
28
40
  email:
29
41
  - ded@ddoherty.net
30
42
  executables: []
@@ -32,10 +44,11 @@ extensions: []
32
44
  extra_rdoc_files: []
33
45
  files:
34
46
  - ".envrc"
35
- - ".github/workflows/gem-push.yml"
47
+ - ".github/workflows/ruby.yml"
36
48
  - ".gitignore"
37
49
  - ".rspec"
38
- - ".travis.yml"
50
+ - ".rubocop.yml"
51
+ - CHANGELOG.org
39
52
  - Gemfile
40
53
  - LICENSE.txt
41
54
  - README.org
@@ -50,7 +63,6 @@ files:
50
63
  homepage: https://github.com/ddoherty03/fat_period
51
64
  licenses: []
52
65
  metadata: {}
53
- post_install_message:
54
66
  rdoc_options: []
55
67
  require_paths:
56
68
  - lib
@@ -65,8 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
77
  - !ruby/object:Gem::Version
66
78
  version: '0'
67
79
  requirements: []
68
- rubygems_version: 3.5.22
69
- signing_key:
80
+ rubygems_version: 3.6.3
70
81
  specification_version: 4
71
82
  summary: Implements a Period class as a Range of Dates.
72
83
  test_files: []
@@ -1,48 +0,0 @@
1
- name: Ruby Gem
2
-
3
- on:
4
- push:
5
- branches: [ "master" ]
6
- pull_request:
7
- branches: [ "master" ]
8
-
9
- jobs:
10
- build:
11
- name: Build + Publish
12
- runs-on: ubuntu-latest
13
- permissions:
14
- contents: read
15
- packages: write
16
-
17
- steps:
18
- - uses: actions/checkout@v4
19
- - name: Set up Ruby 2.6
20
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
21
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
22
- # uses: ruby/setup-ruby@v1
23
- uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
24
- with:
25
- ruby-version: 2.6.x
26
-
27
- - name: Publish to GPR
28
- run: |
29
- mkdir -p $HOME/.gem
30
- touch $HOME/.gem/credentials
31
- chmod 0600 $HOME/.gem/credentials
32
- printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
33
- gem build *.gemspec
34
- gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
35
- env:
36
- GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
37
- OWNER: ${{ github.repository_owner }}
38
-
39
- - name: Publish to RubyGems
40
- run: |
41
- mkdir -p $HOME/.gem
42
- touch $HOME/.gem/credentials
43
- chmod 0600 $HOME/.gem/credentials
44
- printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
45
- gem build *.gemspec
46
- gem push *.gem
47
- env:
48
- GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
data/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.5
4
- - 2.6
5
- - 2.7
6
- - 3.0