- Version
- Introduction
- Installation
- Installing the gem
- Usage
- Constant Period::FOREVER
- Construction of Periods
- Period.new
- Period.parse
- Period.parse_phrase
- Period.ensure
- Conversion
- To Range
- To String
- TeX Form
- Comparison
- Enumeration
- Size
- Chunking
- Set Operations
- Subset Determination
- Superset Determination
- Intersection
- Difference
- Union
- Coverage
- Contains?
- Overlapping
- Spanning
- Gaps
- Development
- Contributing
Version
"Current version is: #{FatPeriod::VERSION}"Current version is: 3.0.1
Introduction
FatPeriod provides a Ruby Period class for dealing with time periods of days, that is ranges whose endpoints are of class Date. It's target is financial applications, but it serves well for any application where periods of time are useful. It builds on the fat_date gem, which provides enhancements to the Date class, especially its class method Date.spec for interpreting a rich set of "specs" as the beginning or end of a variety of calendar-related periods.
In addition, set operations are provided for Period, as well as methods for breaking a larger periods into an array of smaller periods of various 'chunk' sizes that correspond to calendar-related periods such as days, weeks, months, and so forth.
Installation
Installing the gem
Add this line to your application's Gemfile:
gem 'fat_period'true
And then execute:
$ bundleOr install it yourself as:
$ gem install fat_periodUsage
Constant Period::FOREVER
The Period class depends on the extensions to Date made by the fat_core gem, which you can read about here. It defines a constant, Period::FOREVER, which is defined as extending from Date::BOT to Date::EOT, which are defined in fat_date as 1900-01-01 and 3000-12-31, respectively and define the beginning of time and end of time for practical commercial purposes. The constant is not frozen, so you can re-define it to your liking.
Construction of Periods
Period.new
A Period is constructed with two arguments for the begin and end date. The begin date must be on or before the end date. Each argument can be (1) a Date, (2) a string parseable as a Date by the Date.parse method, or (3) an object that responds to #to_s and can be parsed as a Date by Date.parse:
p1 = Period.new(Date.today, Date.today + 30)
p2 = Period.new('Nov 22, 1963', Date.today)
p3 = Period.new('1961-01-21', '1963-11-22')
[[p1.to_s], [p2.to_s], [p3.to_s]]| 2025-12-24 to 2026-01-23 |
| 1963-11-22 to 2025-12-24 |
| 1961-01-21 to 1963-11-22 |
["Camelot lasted #{p3.length} days"]| Camelot lasted 1036 days |
Period.parse
A more convenient way to construct a period is provided by Period.parse. It takes two strings as its arguments, a mandatory "from-spec" and an optional "to-spec":
A "spec" is a string designating some period of time. There are many ways of specifying a period, which are detailed below.
-
With Only a From-Spec
If only a from-spec is given, it defines both the beginning and end of the overall period:
tab = [] tab << ['From Spec', 'Result'] tab << nil froms = ['2020', '2020-2Q', '2020-W15', '2020-09', '2020-09-A', '2020-09-iii'] froms.each do |f| tab << [f, Period.parse(f).inspect] end tab
| From Spec | Result | |-------------+--------------------------------| | 2020 | Period(2020-01-01..2020-12-31) | | 2020-2Q | Period(2020-04-01..2020-06-30) | | 2020-W15 | Period(2020-04-06..2020-04-12) | | 2020-09 | Period(2020-09-01..2020-09-30) | | 2020-09-A | Period(2020-09-01..2020-09-15) | | 2020-09-iii | Period(2020-09-14..2020-09-20) || From Spec | Result | |-------------+--------------------------------| | 2020 | Period(2020-01-01..2020-12-31) | | 2020-2Q | Period(2020-04-01..2020-06-30) | | 2020-W15 | Period(2020-04-06..2020-04-12) | | 2020-09 | Period(2020-09-01..2020-09-30) | | 2020-09-A | Period(2020-09-01..2020-09-15) | | 2020-09-iii | Period(2020-09-14..2020-09-20) | -
With Both a From-Spec and To-Spec
But, if a to-spec is also given, the from-spec defines the beginning of the period and the to-spec defines the end of the period. In particular, the beginning of the period is the first day of the from-spec and the end of the period is the last day of the to-spec:
tab = [] tab << ['From Spec', 'To Spec', 'Result'] tab << nil from_tos = [['2020', '2020-2Q'], ['2020-2Q', '2020-W15'], ['2020-W15', '2020-09'], ['2020-09', '2020-09-A'], ['2020-09-A', '2020-09-iii']] from_tos.each do |f, t| tab << [f, t, Period.parse(f, t).inspect] end tab
| From Spec | To Spec | Result | |-----------+-------------+--------------------------------| | 2020 | 2020-2Q | Period(2020-01-01..2020-06-30) | | 2020-2Q | 2020-W15 | Period(2020-04-01..2020-04-12) | | 2020-W15 | 2020-09 | Period(2020-04-06..2020-09-30) | | 2020-09 | 2020-09-A | Period(2020-09-01..2020-09-15) | | 2020-09-A | 2020-09-iii | Period(2020-09-01..2020-09-20) || From Spec | To Spec | Result | |-----------+-------------+--------------------------------| | 2020 | 2020-2Q | Period(2020-01-01..2020-06-30) | | 2020-2Q | 2020-W15 | Period(2020-04-01..2020-04-12) | | 2020-W15 | 2020-09 | Period(2020-04-06..2020-09-30) | | 2020-09 | 2020-09-A | Period(2020-09-01..2020-09-15) | | 2020-09-A | 2020-09-iii | Period(2020-09-01..2020-09-20) | -
Using Skip Modifiers
One new feature of FatDate is the ability to add a "skip modifier" to the end of a date spec to skip forward or backward to the first day-of-week either on or before/after the date given by the spec. For example, the following demonstrates that one can set the 'to' spec to the last Wednesday of 2025 or the last Wednesday before the end of 2025. Using '>' or '>=' specified skipping forward instead.
tab = [] tab << ['From Spec', 'To Spec', 'Result', 'Description'] tab << nil from_to_descs = [['2025-2Q', '2025<=Wed', 'From 2q to last Wednesday of 2025'], ['2025-2Q', '2025<Wed', 'From 2q to last Wednesday /before/ the end of 2025'], ['2012-11', '2012-11<=Thur', 'November 2012 through last Thursday'], ['2012-11', '2012-11-4Thur', 'And through Thanksgiving (not always the /last/ Thursday!)'] ] from_to_descs.each do |f, t, d| tab << [f, t, Period.parse(f, t).inspect, d] end tab
| From Spec | To Spec | Result | Description | |-----------+---------------+--------------------------------+------------------------------------------------------------| | 2025-2Q | 2025<=Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday of 2025 | | 2025-2Q | 2025<Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday /before/ the end of 2025 | | 2012-11 | 2012-11<=Thur | Period(2012-11-01..2012-11-29) | November 2012 through last Thursday | | 2012-11 | 2012-11-4Thur | Period(2012-11-01..2012-11-22) | And through Thanksgiving (not always the /last/ Thursday!) || From Spec | To Spec | Result | Description | |-----------+---------------+--------------------------------+------------------------------------------------------------| | 2025-2Q | 2025<=Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday of 2025 | | 2025-2Q | 2025<Wed | Period(2025-04-01..2025-12-31) | From 2q to last Wednesday /before/ the end of 2025 | | 2012-11 | 2012-11<=Thur | Period(2012-11-01..2012-11-29) | November 2012 through last Thursday | | 2012-11 | 2012-11-4Thur | Period(2012-11-01..2012-11-22) | And through Thanksgiving (not always the /last/ Thursday!) |
Period.parse_phrase
For example:
The Period.parse_phrase method will take a string having a 'from', 'to', and 'per' clause and return an Array of Periods encompassing the same period as Period.parse, but optionally broken into sub-periods each having the length specified by the 'per' clause. Period.parse_phrase always returns an Array of Periods even if there is no 'per' clause and the Array has only one member. If there is no 'to' clause, the returned period is from the start of the 'from' period to its end. If there is neither a 'from' or a 'to' clause, it tries to interpret the beginning of the phrase as a valid spec and uses it as a 'from' clause.
tab = []
tab << ['k', 'Sub Period']
tab << nil
pds = Period.parse_phrase('from 2025 to 2025-3Q per month')
pds.each_with_index do |pd, k|
tab << [k, pd.inspect]
end
tab| k | Sub Period |
|---+--------------------------------|
| 0 | Period(2025-01-01..2025-01-31) |
| 1 | Period(2025-02-01..2025-02-28) |
| 2 | Period(2025-03-01..2025-03-31) |
| 3 | Period(2025-04-01..2025-04-30) |
| 4 | Period(2025-05-01..2025-05-31) |
| 5 | Period(2025-06-01..2025-06-30) |
| 6 | Period(2025-07-01..2025-07-31) |
| 7 | Period(2025-08-01..2025-08-31) |
| 8 | Period(2025-09-01..2025-09-30) |
| k | Sub Period |
|---+--------------------------------|
| 0 | Period(2025-01-01..2025-01-31) |
| 1 | Period(2025-02-01..2025-02-28) |
| 2 | Period(2025-03-01..2025-03-31) |
| 3 | Period(2025-04-01..2025-04-30) |
| 4 | Period(2025-05-01..2025-05-31) |
| 5 | Period(2025-06-01..2025-06-30) |
| 6 | Period(2025-07-01..2025-07-31) |
| 7 | Period(2025-08-01..2025-08-31) |
| 8 | Period(2025-09-01..2025-09-30) |
The period named in the 'per' clause is called a 'chunk' and there are several valid chunk names in FatPeriod:
| Chunk Name |
|---|
| year |
| half |
| quarter |
| bimonth |
| month |
| semimonth |
| biweek |
| week |
| day |
Here is the same period broken into weeks. Notice that the first and last "weeks" are not whole weeks because parts of them fall outside the boundaries of the overall period.
tab = []
tab << ['k', 'Sub Period']
tab << nil
pds = Period.parse_phrase('from 2025 to 2025-3Q per week')
pds.each_with_index do |pd, k|
tab << [k, pd.inspect]
end
tab| k | Sub Period |
|----+--------------------------------|
| 0 | Period(2025-01-01..2025-01-05) |
| 1 | Period(2025-01-06..2025-01-12) |
| 2 | Period(2025-01-13..2025-01-19) |
| 3 | Period(2025-01-20..2025-01-26) |
| 4 | Period(2025-01-27..2025-02-02) |
| 5 | Period(2025-02-03..2025-02-09) |
| 6 | Period(2025-02-10..2025-02-16) |
| 7 | Period(2025-02-17..2025-02-23) |
| 8 | Period(2025-02-24..2025-03-02) |
| 9 | Period(2025-03-03..2025-03-09) |
| 10 | Period(2025-03-10..2025-03-16) |
| 11 | Period(2025-03-17..2025-03-23) |
| 12 | Period(2025-03-24..2025-03-30) |
| 13 | Period(2025-03-31..2025-04-06) |
| 14 | Period(2025-04-07..2025-04-13) |
| 15 | Period(2025-04-14..2025-04-20) |
| 16 | Period(2025-04-21..2025-04-27) |
| 17 | Period(2025-04-28..2025-05-04) |
| 18 | Period(2025-05-05..2025-05-11) |
| 19 | Period(2025-05-12..2025-05-18) |
| 20 | Period(2025-05-19..2025-05-25) |
| 21 | Period(2025-05-26..2025-06-01) |
| 22 | Period(2025-06-02..2025-06-08) |
| 23 | Period(2025-06-09..2025-06-15) |
| 24 | Period(2025-06-16..2025-06-22) |
| 25 | Period(2025-06-23..2025-06-29) |
| 26 | Period(2025-06-30..2025-07-06) |
| 27 | Period(2025-07-07..2025-07-13) |
| 28 | Period(2025-07-14..2025-07-20) |
| 29 | Period(2025-07-21..2025-07-27) |
| 30 | Period(2025-07-28..2025-08-03) |
| 31 | Period(2025-08-04..2025-08-10) |
| 32 | Period(2025-08-11..2025-08-17) |
| 33 | Period(2025-08-18..2025-08-24) |
| 34 | Period(2025-08-25..2025-08-31) |
| 35 | Period(2025-09-01..2025-09-07) |
| 36 | Period(2025-09-08..2025-09-14) |
| 37 | Period(2025-09-15..2025-09-21) |
| 38 | Period(2025-09-22..2025-09-28) |
| 39 | Period(2025-09-29..2025-09-30) |
| k | Sub Period |
|----+--------------------------------|
| 0 | Period(2025-01-01..2025-01-05) |
| 1 | Period(2025-01-06..2025-01-12) |
| 2 | Period(2025-01-13..2025-01-19) |
| 3 | Period(2025-01-20..2025-01-26) |
| 4 | Period(2025-01-27..2025-02-02) |
| 5 | Period(2025-02-03..2025-02-09) |
| 6 | Period(2025-02-10..2025-02-16) |
| 7 | Period(2025-02-17..2025-02-23) |
| 8 | Period(2025-02-24..2025-03-02) |
| 9 | Period(2025-03-03..2025-03-09) |
| 10 | Period(2025-03-10..2025-03-16) |
| 11 | Period(2025-03-17..2025-03-23) |
| 12 | Period(2025-03-24..2025-03-30) |
| 13 | Period(2025-03-31..2025-04-06) |
| 14 | Period(2025-04-07..2025-04-13) |
| 15 | Period(2025-04-14..2025-04-20) |
| 16 | Period(2025-04-21..2025-04-27) |
| 17 | Period(2025-04-28..2025-05-04) |
| 18 | Period(2025-05-05..2025-05-11) |
| 19 | Period(2025-05-12..2025-05-18) |
| 20 | Period(2025-05-19..2025-05-25) |
| 21 | Period(2025-05-26..2025-06-01) |
| 22 | Period(2025-06-02..2025-06-08) |
| 23 | Period(2025-06-09..2025-06-15) |
| 24 | Period(2025-06-16..2025-06-22) |
| 25 | Period(2025-06-23..2025-06-29) |
| 26 | Period(2025-06-30..2025-07-06) |
| 27 | Period(2025-07-07..2025-07-13) |
| 28 | Period(2025-07-14..2025-07-20) |
| 29 | Period(2025-07-21..2025-07-27) |
| 30 | Period(2025-07-28..2025-08-03) |
| 31 | Period(2025-08-04..2025-08-10) |
| 32 | Period(2025-08-11..2025-08-17) |
| 33 | Period(2025-08-18..2025-08-24) |
| 34 | Period(2025-08-25..2025-08-31) |
| 35 | Period(2025-09-01..2025-09-07) |
| 36 | Period(2025-09-08..2025-09-14) |
| 37 | Period(2025-09-15..2025-09-21) |
| 38 | Period(2025-09-22..2025-09-28) |
| 39 | Period(2025-09-29..2025-09-30) |
Period.ensure
Period.ensure tries to interpret its argument as a Period and returns it; otherwise it throws an ArgumentError exception:
- if the argument responds to the
to_periodmethod, it invokes that on the argument and returns it; - if the argument is a
String, it usesPeriod.parse_phraseto try to interepret it as aPeriod; - if it is already a
Period, it just returns the argument; - otherwise, it throws an
ArgumentErrorexception.
class ContainingMonth
def initialize(dat)
@dat = Date.ensure(dat)
end
def to_period
Period.month_containing(@dat)
end
end
cm = ContainingMonth.new('2025-09-22')
Period.ensure(cm)Period(2025-09-01..2025-09-30)
Period(2025-09-01..2025-09-30)
Period.ensure('from 2016 to 2018-3Q')Period(2016-01-01..2018-09-30)
Period(2016-01-01..2018-09-30)
Period.ensure(Period.new('2016-01-02', '2017-09-29'))Period(2016-01-02..2017-09-29)
Period(2016-01-02..2017-09-29)
Conversion
To Range
To String
TeX Form
Comparison
Enumeration
Size
Chunking
Set Operations
Subset Determination
Superset Determination
Intersection
Difference
Union
Coverage
Contains?
Overlapping
Spanning
Gaps
Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/ddoherty03/fat_table.