[[https://travis-ci.org/ddoherty03/fat_core.svg?branch=master]]
fat-core is a simple gem to collect core extensions and a few new classes
that I find useful in multiple projects. The emphasis is on extending the
Date class to make it more useful in financial applications.
** Installation
Add this line to your application's Gemfile:
#+begin_SRC ruby
gem 'fat_core', :git => 'https://github.com/ddoherty03/fat_core.git'
#+end_SRC
And then execute:
#+begin_src shell
$ bundle
#+end_src
Or install it yourself as:
#+begin_src shell
$ gem install fat_core
#+end_src
** Usage
You can extend classes individually by requiring the corresponding file:
#+begin_SRC ruby
require 'fat_core/array'
require 'fat_core/bigdecimal'
require 'fat_core/date'
require 'fat_core/enumerable'
require 'fat_core/hash'
require 'fat_core/kernel'
require 'fat_core/numeric'
require 'fat_core/range'
require 'fat_core/string'
require 'fat_core/symbol'
#+end_SRC
Or, you can require them all:
#+begin_SRC ruby
require 'fat_core/all'
#+end_SRC
Many of these have little that is of general interest, but there are a few
goodies.
*** Date
**** Constants
FatCore adds two date constants to the Date class, Date::BOT and
Date::EOT. These represent the earliest and latest dates of practical
commercial interest. The exact values are rather arbitrary, but they prove
useful in date ranges, for example. They are defined as:
Date::BOT :: January 1, 1900Date::EOT :: December 31, 3000Date::FEDERAL_DECREED_HOLIDAYS :: an Array of dates declared as non-work
days for federal employees by presidential proclamationDate::PRESIDENTIAL_FUNERALS :: an Array of dates of presidential funerals,
which are observed with a closing of most federal agencies
**** Ensure
The Date.ensure class method tries to convert its argument to a Date
object by (1) applying the #to_date method or (2) applying the Date.parse
method to a String. This is handy when you want to define a method that takes
a date argument but want the caller to be able to supply anything that can
reasonably be converted to a Date:
#+begin_src ruby
$:.unshift("~/src/fat_core/lib")
require 'fat_core/date' # => true
def tomorow_tomorrow(arg)
from = Date.ensure(arg) # => ArgumentError: cannot convert class 'Array' to a Date or DateTime
from + 2.days # => Mon, 03 Jun 2024, Wed, 16 Oct 2024 05:47:30 -0500, Sun, 03 Mar 2024
end # => :tomorow_tomorrow
tomorow_tomorrow('June 1') # => Mon, 03 Jun 2024
tomorow_tomorrow(Time.now) # => Wed, 16 Oct 2024 05:47:30 -0500
But it's only as good as Date.parse!
tomorow_tomorrow('Ides of March') # => Sun, 03 Mar 2024
tomorow_tomorrow([])
=>
~> ArgumentError
~> cannot convert class 'Array' to a Date or DateTime
~>
~> /home/ded/src/fat_core/lib/fat_core/date.rb:1849:in `ensure_date'
~> /home/ded/src/fat_core/lib/fat_core/date.rb:1863:in `ensure'
~> /tmp/seeing_is_believing_temp_dir20241014-1457038-xj4k5x/program.rb:5:in `tomorow_tomorrow'
~> /tmp/seeing_is_believing_temp_dir20241014-1457038-xj4k5x/program.rb:14:in `'
#+end_src
**** Formatting
FatCore provides some concise methods for printing string versions of dates
that are often useful:
#+begin_SRC ruby :results output :wrap example :exports both
require 'fat_core/date'
d = Date.parse('1957-09-22')
puts "ISO: #{d.iso}"
puts "All Numbers: #{d.num}"
puts "Emacs Org Mode Inactive: #{d.org}"
puts "Emacs Org Mode Active: #{d.org(true)}"
puts "LaTeX: #{d.tex_quote}"
puts "English: #{d.eng}"
puts "American: #{d.american}"
#+end_SRC
#+begin_example
ISO: 1957-09-22
All Numbers: 19570922
Emacs Org Mode Inactive: [1957-09-22 Sun]
Emacs Org Mode Active: <1957-09-22 Sun>
LaTeX: 1957--09--22
English: September 22, 1957
American: 9/22/1957
#+end_example
Most of these are self-explanatory, but a couple are not. The #org method
formats a date as an Emacs org-mode timestamp, by default an inactive
timestamp that does not show up in the org agenda, but can be made active with
the optional parameter set to a truthy value. See
[[https://orgmode.org/manual/Timestamps.html#Timestamps]].
The #tex_quote method formats the date in iso form but using TeX's
convention of using en-dashes to separate the components.
**** Chunks
Many of the methods provided by FatCore deal with various calendar periods
that are less common than those provided by the Ruby Standard Library or gems
such as active_support. This documentation refers to these calendar periods
as "chunks", and they are the following:
- year,
- half,
- quarter,
- bimonth,
- month,
- semimonth,
- biweek,
- week, and
- day
FatCore provides methods that query whether the date falls on the beginning
or end of each of these chunks:
#+begin_SRC ruby :wrap example :exports both
require 'fat_core/date'
tab = []
d = Date.parse('2017-06-30')
%i[beginning end].each do |side|
%i(year half quarter bimonth month semimonth biweek week).each do |chunk|
meth = "#{side}of#{chunk}?".to_sym
tab << [d.iso, meth.to_s, "#{d.send(meth)}"]
end
end
tab
#+end_SRC
#+RESULTS:
#+begin_example
| 2017-06-30 | beginning_of_year? | false |
| 2017-06-30 | beginning_of_half? | false |
| 2017-06-30 | beginning_of_quarter? | false |
| 2017-06-30 | beginning_of_bimonth? | false |
| 2017-06-30 | beginning_of_month? | false |
| 2017-06-30 | beginning_of_semimonth? | false |
| 2017-06-30 | beginning_of_biweek? | false |
| 2017-06-30 | beginning_of_week? | false |
| 2017-06-30 | end_of_year? | false |
| 2017-06-30 | end_of_half? | true |
| 2017-06-30 | end_of_quarter? | true |
| 2017-06-30 | end_of_bimonth? | true |
| 2017-06-30 | end_of_month? | true |
| 2017-06-30 | end_of_semimonth? | true |
| 2017-06-30 | end_of_biweek? | false |
| 2017-06-30 | end_of_week? | false |
#+end_example
It also provides corresponding methods that return the date at the beginning
or end of the calendar chunk, starting at the given date:
#+begin_SRC ruby :wrap example :exports both
require 'fat_core/date'
tab = []
d = Date.parse('2017-04-21')
%i[beginning end].each do |side|
%i(year half quarter bimonth month semimonth biweek week ).each do |chunk|
meth = "#{side}of#{chunk}".to_sym
tab << [d.iso, "d.#{meth}", "#{d.send(meth)}"]
end
end
tab
#+end_SRC
#+RESULTS:
#+begin_example
| 2017-04-21 | d.beginning_of_year | 2017-01-01 |
| 2017-04-21 | d.beginning_of_half | 2017-01-01 |
| 2017-04-21 | d.beginning_of_quarter | 2017-04-01 |
| 2017-04-21 | d.beginning_of_bimonth | 2017-03-01 |
| 2017-04-21 | d.beginning_of_month | 2017-04-01 |
| 2017-04-21 | d.beginning_of_semimonth | 2017-04-16 |
| 2017-04-21 | d.beginning_of_biweek | 2017-04-10 |
| 2017-04-21 | d.beginning_of_week | 2017-04-17 |
| 2017-04-21 | d.end_of_year | 2017-12-31 |
| 2017-04-21 | d.end_of_half | 2017-06-30 |
| 2017-04-21 | d.end_of_quarter | 2017-06-30 |
| 2017-04-21 | d.end_of_bimonth | 2017-04-30 |
| 2017-04-21 | d.end_of_month | 2017-04-30 |
| 2017-04-21 | d.end_of_semimonth | 2017-04-30 |
| 2017-04-21 | d.end_of_biweek | 2017-04-23 |
| 2017-04-21 | d.end_of_week | 2017-04-23 |
#+end_example
You can query which numerical half, quarter, etc. that a given date falls in:
#+begin_SRC ruby :exports both :wrap example
require 'fat_core/date'
tab = []
%i(year half quarter bimonth month semimonth biweek week ).each do |chunk|
d = Date.parse('2017-04-21') + rand(100)
meth = "#{chunk}".to_sym
tab << [d.iso, "d.#{meth}", "in #{chunk} number #{d.send(meth)}"]
end
tab
#+end_SRC
#+RESULTS:
#+begin_example
| 2017-07-05 | d.year | in year number 2017 |
| 2017-06-03 | d.half | in half number 1 |
| 2017-05-30 | d.quarter | in quarter number 2 |
| 2017-07-08 | d.bimonth | in bimonth number 4 |
| 2017-06-28 | d.month | in month number 6 |
| 2017-05-14 | d.semimonth | in semimonth number 9 |
| 2017-07-25 | d.biweek | in biweek number 15 |
| 2017-06-19 | d.week | in week number 25 |
#+end_example
**** Parsing
FatCore also adds some convenience methods for parsing strings as Date
objects.
***** American Dates
Americans often write dates in the form M/d/Y, and the normal parse method
will parse such a string as d/M/Y, often resulting in invalid date errors.
FatCore adds the specialty parsing method, Date.parse_american to handle
such strings.
#+begin_SRC ruby :results output :exports both :wrap example
require 'fat_core/date'
begin
ss = '9/22/1957'
Date.parse(ss)
rescue Date::Error => ex
puts "Date.parse('#{ss}') raises #{ex.class} (#{ex}), but"
puts "Date.parse_american('#{ss}') => #{Date.parse_american(ss)}"
end
#+end_SRC
#+RESULTS:
#+begin_example
Date.parse('9/22/1957') raises Date::Error (invalid date), but
Date.parse_american('9/22/1957') => 1957-09-22
#+end_example
***** Date Specs
It is often desirable to get the first or last date of a specified time
period. For this FatCore provides the parse_spec method that takes a
string and an optional spec_type parameter of either :from, indicating
that the first date of the period should be returned or :to, indicating that
the last date of the period should be returned.
This method supports a rich set of ways to specify periods of time:
- YYYY-MM-DD :: returns a single day as the time period,
- YYYY-MM :: returns the specified month, beginning or end
- YYYY :: returns the specified year, beginning or end
- YYYY-ddd :: returns the ddd'th day of the specified year, beginning or end
- MM :: returns the specified month of the current year, beginning or end
- MM-DD :: returns the specified day of the specified month in the current
year, beginning or end,
- YYYY-Wnn or YYYY-nnW :: returns the nn'th commercial week of the given year
according to the ISO 8601 standard, in which the week containing the first
Thursday of the year counts as the first commercial week, even if that week
started in the prior calendar year,
- Wnn or nnW :: returns the nn'th commercial week of the current year,
- YYYY-1H or YYYY-2H :: returns the specified half year for the given year,
- 1H or 2H :: returns the specified half year for the current year,
- YYYY-1Q, YYYY-2Q, etc :: returns the calendar quarter for the given year,
- 1Q, 2Q, etc :: returns the calendar quarter for the current year,
- YYYY-MM-I or YYYY-MM-II :: returns the semi-month for the given month and
year, where the first semi-month always runs from the 1st to the 15th and
the second semi-month always runs from the 16th to the last day of the given
month, regardless of the number of days in the month,
- YYYY-MM-i or YYYY-MM-ii up to YYYY-MM-vi :: returns the given week within
the month, including any partial weeks,
- MM-i or MM-ii up to MM-vi :: returns the given week within the month of the
current year, including any partial weeks,
- i or ii up to vi :: returns the given week within the current month of the current
year, including any partial weeks,
- YYYY-MM-nSu up to YYYY-MM-nSa :: returns the single date that is the n'th
Sunday, Monday, etc., in the given month using the first two letters of the
English names for the days of the week,
- MM-nSu up to MM-nSa :: returns the single date that is the n'th Sunday,
Monday, etc., in the given month of the current year using the first two
letters of the English names for the days of the week,
- nSu up to nSa :: returns the single date that is the n'th Sunday, Monday,
etc., in the current month of the current year using the first two letters
of the English names for the days of the week,
- YYYY-nnn :: is the nnn'th day of the given year, exactly three digits needed,
- nnn :: is the nnn'th day of the current year, exactly three digits needed,
- YYYY-E :: returns the single date of Easter in the Western church for the
given year,
- E :: returns the single date of Easter in the Western church for the current
year,
- YYYY-E-n or YYYY-E+n :: returns the single date that falls n days before (-)
or after (+) Easter in the Western church for the given year,
- E-n or E+n :: returns the single date that falls n days before (-) or after
(+) Easter in the Western church for the current year,
- yesterday or yesteryear or lastday or last_year, etc :: the relative
prefixes, 'last' or 'yester' prepended to any chunk name returns the period
named by the chunk that precedes today's date.
- today or toyear or this-year or thissemimonth, etc :: the relative prefixes,
'to' or 'this' prepended to any chunk name returns the period named by
the chunk that contains today's date.
- nextday or nextyear or next-year or nextsemimonth, etc :: the relative
prefixes, 'next' prepended to any chunk name returns the period named by the
chunk that follows today's date. As a special case, 'tomorrow' is treated as
equivalent to 'nextday'.
- forever :: returns the period Date::BOT to Date::EOT, which, for financial
applications is meant to stand in for eternity.
- never :: returns nil, representing no date.
Some things to note with respect to Date.parse_spec:
- The second argument should be either
:from or :to, but it defaults to
:from. If it is :from, parse_spec returns the first date of the
specified period; if it is :to, it returns the last date of the specified
period. When the "period" resolves to a single day, both arguments return
the same date, so parse_spec('2024-E', :from) and parse_spec('2024-E',
:to) both result in March 31, 2024. - Where relevant,
parse_spec accepts letters of either upper or lower case:
so 2024-1Q can be written 2024-1q and 'yesteryear' can be written
'YeSterYeaR', and likewise for all components of the spec using letters. - Date components can be separated with either a hyphen, as in the examples
above, or with a '/' as is common. Thus, 2024-11-09 can also be
2024/11/09, or indeed, 2024/11-09 or 2024-11/09.
- The prefixes for relative periods can be separated from the period name by
a hyphen, and underscore, or by nothing at all. Thus, yester-day,
yester_day, and yesterday are all acceptable. Clearly neologisms such as
'yestermonth' are quaint, but not harmful.
- On the other hand, to get a day-of-year spec right, you must use exactly 3
digits: 2024-011 is the 11th day of 2024, but 2024-11 is November of 2024.
**** Holidays and Workdays
One of the original motivations for this library was to provide an easy way to
determine whether a given date is a federal holiday in the United States or,
nearly but not quite the same, a non-trading day on the New York Stock
Exchange. To that end, FatCore provides the following methods:
- Date#weekend? -- is this date on a weekend?
- Date#weekday? -- is this date on a week day?
- Date#easter_this_year -- the date of Easter in the Date's year
Methods concerning Federal holidays:
- Date#fed_holiday? -- is this date a Federal holiday? It knows about
obscurities such as holidays decreed by past Presidents, dates of
Presidential funerals, and the Federal rule for when holidays fall on a
weekend, whether it is moved to the prior Friday or the following Monday.
- Date#fed_workday? -- is it a date when the Federal government is open?,
inverse of Date#fed_holiday?
- Date#add_fed_workdays(n) -- n Federal workdays following (or preceding if n
negative) this date,
- Date#next_fed_workday -- the next Federal workday following this date,
- Date#prior_fed_workday -- the previous Federal workday before this date,
- Date#next_until_fed_workday -- starting with this date, move forward until
we hit a Federal workday
- Date#prior_until_fed_workday -- starting with this date, move back until
we hit a Federal workday
And we have similar methods for "holidays" or non-trading days on the NYSE:
- Date#nyse_holiday? -- is this date a NYSE holiday?
- Date#nyse_workday? -- is it a date when the NYSE is open for trading?,
inverse of Date#nyse_holiday?
- Date#add_nyse_workdays(n) -- n NYSE workdays following (or preceding if n
negative) this date,
- Date#next_nyse_workday -- the next NYSE workday following this date,
- Date#prior_nyse_workday -- the previous NYSE workday before this date,
- Date#next_until_nyse_~~workday -- starting with this date, move forward until
we hit a NYSE workday
- Date#prior_until_nyse_workday -- starting with this date, move back until
we hit a Federal workday
**** Ordinal Weekdays in Month
It is often useful to find the 1st, 2nd, etc, Sunday, Monday, etc. in a given
month. FatCore provides the class method Date.nth_wday_in_year_month(nth,
wday, year, month) to return such dates. The first parameter can be
negative, which will count from the end of the month.
**** Easter
The Date class extension adds two methods for determining whether a given
date is a US federal holiday as defined by federal law, including such things
as federal holidays established by executive decree:
#+begin_SRC ruby
require 'fat_core/date'
Date.parse('2014-05-18').fed_holiday? => true # It's a weekend
Date.parse('2014-01-01').fed_holiday? => true # It's New Years
#+end_SRC
Likewise, days on which the NYSE is closed can be gotten with:
#+begin_SRC ruby
Date.parse('2014-04-18').nyse_holiday? => true # It's Good Friday
#+end_SRC
Conversely, Date#fed_workday? and Date#nyse_workday? return true if the
federal government and the NYSE respectively are open for business on those
days.
In addition, the Date class, as extended by FatCore, adds #next_
methods for calendar periods in addition to those provided by the core Date
class: #next_half, #next_quarter, #next_bimonth, and #next_semimonth,
#next_biweek. There are also #prior_ variants of these, as well as
methods for finding the end and beginning of all these periods (e.g.,
#beginning_of_bimonth) and for querying whether a Date is at the beginning or
end of these periods (e.g., #beginning_of_bimonth?, #end_of_bimonth?, etc.).
FatCore also provides convenience formatting methods, such as Date#iso for
quickly converting a Date to a string of the form 'YYYY-MM-DD', Date#org for
formatting a Date as an Emacs org-mode timestamp, and several others.
Finally, it provides a #parse_spec method for parsing a string, typically
provided by a user, allowing all the period chunks to be conveniently and
tersely specified by a user. For example, the string '2Q' will be parsed as the
second calendar quarter of the current year, while '2014-3Q' will be parsed as
the third quarter of the year 2014.
*** Range
You can also extend the Range class with several useful methods that emphasize
coverage of one range by one or more others (#spanned_by? and #gaps),
contiguity of Ranges to one another (#contiguous?, #left_contiguous?, and
#right_contiguous?, #join), and the testing of overlaps between ranges
(#overlaps?, #overlaps_among?). These are put to good use in the
'fat_period' ([[https://github.com/ddoherty03/fat_period]]) gem, which combines
fat_core's extended Range class with its extended Date class to make a useful
Period class for date ranges, and you may find fat_core's extended Range class
likewise useful.
For example, you can use the #gaps method to find the gaps left in the
coverage on one Range by an Array of other Ranges:
#+begin_SRC ruby
require 'fat_core/range'
(0..12).gaps([(0..2), (5..7), (10..12)]) => [(3..4), (8..9)]
#+end_SRC
- Enumerable
FatCore::Enumerable extends Enumerable with the
#each_with_flags method that
yields the elements of the Enumerable but also yields two booleans, first and
last that are set to true on respectively, the first and last element of the
Enumerable. This makes it easy to treat these two cases specially without
testing the index as in #each_with_index.
*** Hash
FatCore::Hash extends the Hash class with some useful methods for element
deletion (#delete_with_value) and for manipulating the keys
(#keys_with_value, #remap_keys and #replace_keys) of a Hash. It also
provides #each_pair_with_flags as an analog to Enumerable's
#each_with_flags.
It also provides the shovel operator as a convenient alias for Hash#merge,
so that
#+begin_src ruby :tangle no
{a: 'A', b: 'B', c: 'C'} << {c: 'CC', d: 'DD'} << {e: 'EEE'} => {a: 'A', b: 'B', c: 'CC', d: 'DD', e: 'EEE'}
#+end_src
*** String
FatCore::String has methods for performing matching of one string with another
(#matches_with, #fuzzy_match), for converting a string to title-case as
might by used in the title of a book (#entitle), for converting a String into
a useable Symbol (#as_sym) and vice-versa (#as_string also
Symbol#as_string), for wrapping with an optional hanging indent (#wrap),
cleaning up errant spaces (#clean), and computing the Damerau-Levenshtein
distance between strings (#distance). And several others.
*** TeX Quoting
Several of the extension, most notably 'fat_core/string', provides a
#tex_quote method for quoting the string version of an object so as to allow
its inclusion in a TeX document and quote characters such as '$' or '%' that
have a special meaning for TeX.
*** Numbers
FatCore::Numeric has methods for inserting grouping commas into a number
(#commas and #group), for converting seconds to HH:MM:SS.dd format
(#secs_to_hms), for testing for integrality (#whole? and #int_if_whole), and
testing for sign (#signum).
** Contributing
- Fork it ([[http://github.com/ddoherty03/fat_core/fork]] )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request