Module: FatDate::Date::ClassMethods
- Defined in:
- lib/fat_date/date.rb
Utilities collapse
- COMMON_YEAR_DAYS_IN_MONTH =
An Array of the number of days in each month indexed by month number, starting with January = 1, etc.
[ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ].freeze
- GOLDEN_TO_FULLMOON =
Map from "golden number" (y % 19) of year to month and day of Pachal full moon. From https://www.calendarbede.com/book/calculation-julian-easter-sunday
{ 1 => [4, 5], 2 => [3, 25], 3 => [4, 13], 4 => [4, 2], 5 => [3, 22], 6 => [4, 10], 7 => [3, 30], 8 => [4, 18], 9 => [4, 7], 10 => [3, 27], 11 => [4, 15], 12 => [4, 4], 13 => [3, 24], 14 => [4, 12], 15 => [4, 1], 16 => [3, 21], 17 => [4, 9], 18 => [3, 29], 19 => [4, 17], }
Parsing collapse
-
#parse_american(str) ⇒ ::Date
Convert a string
strwith an American style date into a ::Date object. -
#spec(spec, spec_type = :from) ⇒ ::Date
Convert a 'period spec'
specto a ::Date.
Utilities collapse
- #days_in_month(year, month) ⇒ Object
-
#dow_to_wday(s) ⇒ Object
Return the wday number for the given DOW string allowing flexibility on how the days are written.
-
#easter(year, reform_year: 1582) ⇒ ::Date
Return the date of Easter for the Western Church in the given year, accounting for calendar reform.
-
#ensure(dat) ⇒ Date, DateTime
(also: #ensure_date)
Ensure that date is of class Date based either on a string or Date object or an object that responds to #to_date or #to_datetime.
-
#mo_name_to_num(name) ⇒ Integer
Return the 1-indexed integer that corresponds to a month name.
-
#nth_wday_in_year_month(nth, wday, year, month) ⇒ Object
Return the nth weekday in the given month.
-
#roman_to_week(s) ⇒ Object
Return the week number indicated by roman numerals i to vi, regardless of case.
Instance Method Details
#days_in_month(year, month) ⇒ Object
1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 |
# File 'lib/fat_date/date.rb', line 1788 def days_in_month(year, month) raise ArgumentError, 'illegal month number' if month < 1 || month > 12 days = COMMON_YEAR_DAYS_IN_MONTH[month] if month == 2 ::Date.new(year, month, 1).leap? ? 29 : 28 else days end end |
#dow_to_wday(s) ⇒ Object
Return the wday number for the given DOW string allowing flexibility on how the days are written
1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 |
# File 'lib/fat_date/date.rb', line 1891 def dow_to_wday(s) case s.clean when /\ASu/i 0 when /\AMo/i 1 when /\ATu/i 2 when /\AWe/i 3 when /\ATh/i 4 when /\AFr/i 5 when /\ASa/i 6 else raise ArgumentError, "There is no weekday named #{s}" end end |
#easter(year, reform_year: 1582) ⇒ ::Date
Return the date of Easter for the Western Church in the given year, accounting for calendar reform.
1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 |
# File 'lib/fat_date/date.rb', line 1984 def easter(year, reform_year: 1582) require 'date' year = year.to_i y = year # No Easter before the Resurrection! return if year < 30 # Note that since the reform took place in October 1582 in Catholic # Europe and in Spetember 1752 in England if y <= reform_year golden = y % 19 + 1 p_full = ::Date.new( y, GOLDEN_TO_FULLMOON[golden][0], GOLDEN_TO_FULLMOON[golden][1], ) if p_full.wday.zero? p_full + 7.days else easter = p_full easter += 1 until easter.wday.zero? easter end else # Gregorian calendar calculation (original code) a = y % 19 b, c = y.divmod(100) d, e = b.divmod(4) f = (b + 8) / 25 g = (b - f + 1) / 3 h = (19 * a + b - d - g + 15) % 30 i, k = c.divmod(4) l = (32 + 2 * e + 2 * i - h - k) % 7 m = (a + 11 * h + 22 * l) / 451 n, p = (h + l - 7 * m + 114).divmod(31) ::Date.new(y, n, p + 1) end end |
#ensure(dat) ⇒ Date, DateTime Also known as: ensure_date
Ensure that date is of class Date based either on a string or Date object or an object that responds to #to_date or #to_datetime. If the given object is a String, use Date.parse to try to convert it.
If given a DateTime, it returns the unrounded DateTime; if given a Time, it converts it to a DateTime.
2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 |
# File 'lib/fat_date/date.rb', line 2032 def ensure(dat) if dat.is_a?(Date) || dat.is_a?(DateTime) dat elsif dat.is_a?(Time) dat.to_datetime elsif dat.respond_to?(:to_datetime) dat.to_datetime elsif dat.respond_to?(:to_date) dat.to_date elsif dat.is_a?(String) begin ::Date.parse(dat) rescue ::Date::Error raise ArgumentError, "cannot convert string '#{dat}' to a Date or DateTime" end else raise ArgumentError, "cannot convert class '#{dat.class}' to a Date or DateTime" end end |
#mo_name_to_num(name) ⇒ Integer
Return the 1-indexed integer that corresponds to a month name.
1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 |
# File 'lib/fat_date/date.rb', line 1805 def mo_name_to_num(name) case name.clean when /\Ajan/i 1 when /\Afeb/i 2 when /\Amar/i 3 when /\Aapr/i 4 when /\Amay/i 5 when /\Ajun/i 6 when /\Ajul/i 7 when /\Aaug/i 8 when /\Asep/i 9 when /\Aoct/i 10 when /\Anov/i 11 when /\Adec/i 12 end end |
#nth_wday_in_year_month(nth, wday, year, month) ⇒ Object
Return the nth weekday in the given month. If n is negative, count from last day of month.
1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 |
# File 'lib/fat_date/date.rb', line 1841 def nth_wday_in_year_month(nth, wday, year, month) wday = wday.to_i raise ArgumentError, 'illegal weekday number' if wday.negative? || wday > 6 month = month.to_i raise ArgumentError, 'illegal month number' if month < 1 || month > 12 nth = nth.to_i if nth.abs > 5 raise ArgumentError, "#{nth.abs} out of range: can be no more than 5 of any day of the week in any month" end result = if nth.positive? # Set d to the 1st wday in month d = ::Date.new(year, month, 1) d += 1 while d.wday != wday # Set d to the nth wday in month nd = 1 while nd != nth d += 7 nd += 1 end d elsif nth.negative? nth = -nth # Set d to the last wday in month d = ::Date.new(year, month, 1).end_of_month d -= 1 while d.wday != wday # Set d to the nth wday in month nd = 1 while nd != nth d -= 7 nd += 1 end d else raise ArgumentError, 'Argument nth cannot be zero' end dow = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][wday] if result.month != month raise ArgumentError, "There is no #{nth}th #{dow} in #{year}-#{month}" end result end |
#parse_american(str) ⇒ ::Date
Convert a string str with an American style date into a ::Date object
An American style date is of the form MM/DD/YYYY, that is it places the
month first, then the day of the month, and finally the year. The European
convention is typically to place the day of the month first, DD/MM/YYYY.
A date found in the wild can be ambiguous, e.g. 3/5/2014, but a date
string known to be using the American convention can be parsed using this
method. Both the month and the day can be a single digit. The year can be
either 2 or 4 digits, and if given as 2 digits, it adds 2000 to it to give
the year.
1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 |
# File 'lib/fat_date/date.rb', line 1468 def parse_american(str) re = %r{\A\s*(\d\d?)\s*[-/]\s*(\d\d?)\s*[-/]\s*((\d\d)?\d\d)\s*\z} unless str.to_s =~ re raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'" end year = $3.to_i month = $1.to_i day = $2.to_i year += 2000 if year < 100 ::Date.new(year, month, day) end |
#roman_to_week(s) ⇒ Object
Return the week number indicated by roman numerals i to vi, regardless of case.
1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 |
# File 'lib/fat_date/date.rb', line 1914 def roman_to_week(s) case s.clean when /\Ai\z/i 1 when /\Aii\z/i 2 when /\Aiii\z/i 3 when /\Aiv\z/i 4 when /\Av\z/i 5 when /\Avi\z/i 6 else raise ArgumentError, "There is no week number #{s}" end end |
#spec(spec, spec_type = :from) ⇒ ::Date
Convert a 'period spec' spec to a ::Date. A date spec is a short-hand way of
specifying a calendar period either absolutely or relative to the computer
clock. This method returns the first date of that period, when spec_type
is set to :from, the default, and returns the last date of the period
when spec_type is :to.
There are a number of forms the spec can take. In each case,
::Date.spec returns the first date in the period if spec_type is
:from and the last date in the period if spec_type is :to:
YYYY-MM-DDa particular date, so:fromand:toreturn the sameYYYYis the whole yearYYYY,YYYY-1HorYYYY-H1is the first calendar half in yearYYYY,H2or2His the second calendar half of the current year,YYYY-3QorYYYY-Q3is the third calendar quarter of year YYYY,Q3or3Qis the third calendar quarter in the current year,YYYY-04orYYYY-4is April, the fourth month of yearYYYY,4-12or04-12is the 12th of April in the current year,4or04is April in the current year,YYYY-W32orYYYY-32Wis the 32nd week in year YYYY,W32or32Wis the 32nd week in the current year,W32-4or32W-4is the 4th day of the 32nd week in the current year,YYYY-MM-AorYYYY-MM-Bis the first or second half of the given month,YYYY-MM-iorYYYY-MM-vis the first or fifth week of the given month,YYYY-MM-IorYYYY-MM-Vis also the first or fifth week of the given month, i.e., case does not matter,MM-iorMM-vis the first or fifth week of the current month,YYYY-MM-3TuorYYYY-MM-4MoorYYYY-MM-3TueorYYYY-MM-4Monis the third Tuesdsay and fourth Monday of the given month,MM-3TuorMM-4Mo(MM-3TueorMM-4Mon) is the third Tuesdsay and fourth Monday of the given month in the current year,3Tuor4Mois the third Tuesdsay and fourth Monday of the current month,YYYY-Eis Easter of the given year YYYY,Eis Easter of the current year YYYY,YYYY-E+50andYYYY-E-40is 50 days after and 40 days before Easter of the given year,E+50andE-40is 50 days after and 40 days before Easter of the current year,YYYY-001andYYYY-182is first and 182nd day of the given year,001and182is first and 182nd day of the current year,this_<chunk>where<chunk>is one ofyear,half,quarter,bimonth,month,semimonth,biweek,week, orday, the corresponding calendar period in which the current date falls,last_<chunk>where<chunk>is one ofyear,half,quarter,bimonth,month,semimonth,biweek,week, orday, the corresponding calendar period immediately before the one in which the current date falls,todayis the same asthis_day,yesterdayis the same aslast_day,foreveris the period from ::Date::BOT to ::Date::EOT, essentially all dates of commercial interest, andnevercauses the method to return nil.
In all of the above example specs, the letter used for calendar
chunks, W, Q, and H can be written in lower case as well, as can
roman numeral week numbers. Also, you can use / to separate date
components instead of -. Likewise, days of the week can be any
string, upper or lower case, that starts with at least two letters of
the day-of-week name: 'Su', 'su', 'sund', 'sunday', 'surgery', all are
valid ways of writing 'Sunday'.
Each of the foregoing specs may have a 'skip modifier' appended to it for finding the following or preceding day-of-week from that date given by the main spec. A skip modifier is a construction appended to a date spec of the form '<Th', '>Th', '<=Th', or '>=Th' where 'Th' could be the name of any day-of-the-week, as described in the foregoing paragraph. It means that /starting from the date determined by the date spec/, find the first Thursday before (<), on or before (<=), after (>), or on or after (>=) the date. So Thanksgiving in 2028 could be found with Date.spec('2028-11<=Th', :to), i.e., the last Thursday on or before the last day of November, 2028.
1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 |
# File 'lib/fat_date/date.rb', line 1564 def spec(spec, spec_type = :from) spec = spec.to_s.strip.clean unless %i[from to].include?(spec_type) raise ArgumentError, "invalid date spec type: '#{spec_type}'" end today = ::Date.today if (md = spec.match(/(?<dir>[<>]=?)(?<dow>Su|Mo|Tu|We|Th|Fr|Sa)[a-z]*\z/i)) skip_to = md[:dow] skip_dir = md[:dir] spec = spec.sub(/[<>]=?(Su|Mo|Tu|We|Th|Fr|Sa)[a-z]*\z/i, '') else skip_to = nil skip_dir = nil end result = case spec when %r{\A((?<yr>\d\d\d\d)[-/])?(?<doy>\d\d\d)\z} # With 3-digit YYYY-ddd, return the day-of-year year = Regexp.last_match[:yr]&.to_i || ::Date.today.year doy = Regexp.last_match[:doy].to_i max_doy = ::Date.gregorian_leap?(year) ? 366 : 365 if doy > max_doy raise ArgumentError, "invalid day-of-year '#{doy}' (1..#{max_doy}) in '#{spec}'" end ::Date.new(year, 1, 1) + doy - 1 when %r{\A((?<yr>\d\d\d\d)[-/])?(?<mo>\d\d?)([-/](?<dy>\d\d?))?\z} # MM, YYYY-MM, MM-DD year = Regexp.last_match[:yr]&.to_i || ::Date.today.year month = Regexp.last_match[:mo].to_i day = Regexp.last_match[:dy]&.to_i unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month) raise ArgumentError, "invalid month number (1-12): '#{spec}'" end if day ::Date.new(year, month, day) elsif spec_type == :from ::Date.new(year, month, 1) else ::Date.new(year, month, 1).end_of_month end when %r{\A((?<yr>\d\d\d\d)[-/])?(?<wk>\d\d?)W(-(?<dy>\d))?\z}xi, %r{\A((?<yr>\d\d\d\d)[-/])?W(?<wk>\d\d?)(-(?<dy>\d))?\z}xi # Commercial week numbers. The first commercial week of the year is # the one that includes the first Thursday of that year. In the # Gregorian calendar, this is equivalent to the week which includes # January 4. This appears to be the equivalent of ISO 8601 week # number as described at https://en.wikipedia.org/wiki/ISO_week_date year = Regexp.last_match[:yr]&.to_i week_num = Regexp.last_match[:wk].to_i day = Regexp.last_match[:dy]&.to_i unless (1..53).cover?(week_num) raise ArgumentError, "invalid week number (1-53): '#{spec}'" end if day && !(1..7).cover?(day) raise ArgumentError, "invalid ISO day number (1-7): '#{spec}'" end if spec_type == :from ::Date.commercial(year ? year : today.year, week_num, day ? day : 1) else ::Date.commercial(year ? year : today.year, week_num, day ? day : 7) end when %r{^((?<yr>\d\d\d\d)[-/])?(?<qt>\d)[Qq]$}, %r{^((?<yr>\d\d\d\d)[-/])?[Qq](?<qt>\d)$} # Year-Quarter year = Regexp.last_match[:yr]&.to_i || ::Date.today.year quarter = Regexp.last_match[:qt].to_i unless [1, 2, 3, 4].include?(quarter) raise ArgumentError, "invalid quarter number (1-4): '#{spec}'" end month = quarter * 3 if spec_type == :from ::Date.new(year, month, 1).beginning_of_quarter else ::Date.new(year, month, 1).end_of_quarter end when %r{^((?<yr>\d\d\d\d)[-/])?(?<hf>\d)[Hh]$}, %r{^((?<yr>\d\d\d\d)[-/])?[Hh](?<hf>\d)$} # Year-Half year = Regexp.last_match[:yr]&.to_i || ::Date.today.year half = Regexp.last_match[:hf].to_i msg = "invalid half number: '#{spec}'" raise ArgumentError, msg unless [1, 2].include?(half) month = half * 6 if spec_type == :from ::Date.new(year, month, 15).beginning_of_half else ::Date.new(year, month, 1).end_of_half end when /\A(?<yr>\d\d\d\d)\z/ # Year only year = Regexp.last_match[:yr].to_i if spec_type == :from ::Date.new(year, 1, 1) else ::Date.new(year, 12, 31) end when %r{\A(((?<yr>\d\d\d\d)[-\/])?(?<mo>\d\d?)[-\/])?(?<hf_mo>(A|B))\z}i # Year, month, half-month, designated with A or B year = Regexp.last_match[:yr]&.to_i || ::Date.today.year month = Regexp.last_match[:mo]&.to_i || ::Date.today.month hf_mo = Regexp.last_match[:hf_mo] if hf_mo == "A" spec_type == :from ? ::Date.new(year, month, 1) : ::Date.new(year, month, 15) else # hf_mo == "B" spec_type == :from ? ::Date.new(year, month, 16) : ::Date.new(year, month, 16).end_of_month end when %r{\A((?<yr>\d\d\d\d)[-/])?((?<mo>\d\d?)[-/])?(?<wk>(i|ii|iii|iv|v|vi))\z}i # Year, month, week-of-month, partial-or-whole, designated with lowercase Roman year = Regexp.last_match[:yr]&.to_i || ::Date.today.year month = Regexp.last_match[:mo]&.to_i || ::Date.today.month wk = roman_to_week(Regexp.last_match[:wk]) result = if spec_type == :from ::Date.new(year, month, 1).beginning_of_week + (wk - 1).weeks else ::Date.new(year, month, 1).end_of_week + (wk - 1).weeks end # If beginning of week of the 1st is in prior month, return the 1st result = [result, ::Date.new(year, month, 1)].max # If the whole week of the result is in the next month, there was no such week if result.beginning_of_week.month > month msg = sprintf("no week number #{wk} in %04d-%02d", year, month) raise ArgumentError, msg else # But if part of the result week is in this month, return end of month [result, ::Date.new(year, month, 1).end_of_month].min end when %r{\A((?<yr>\d\d\d\d)[-/])?((?<mo>\d\d?)[-/])?((?<nth>[-+]?\d+)(?<dow>(Su|Mo|Tu|We|Th|Fr|Sa)[a-z]*))\z}i # Year, month, ordinal dow name year = Regexp.last_match[:yr]&.to_i || ::Date.today.year month = Regexp.last_match[:mo]&.to_i || ::Date.today.month nth = Regexp.last_match[:nth].to_i wday = dow_to_wday(Regexp.last_match[:dow]) unless (1..12).cover?(month) raise ArgumentError, "invalid month number (1-12): '#{month}' in '#{spec}'" end unless (1..5).cover?(nth.abs) raise ArgumentError, "invalid ordinal day number (1-5): '#{nth}' in '#{spec}'" end ::Date.nth_wday_in_year_month(nth, wday, year, month) when %r{\A(?<yr>\d\d\d\d[-/])?E(?<off>[+-]\d+)?\z}i # Easter for the given year, current year (if no year component), # optionally plus or minus a day offset year = Regexp.last_match[:yr]&.to_i || ::Date.today.year offset = Regexp.last_match[:off]&.to_i || 0 ::Date.easter(year) + offset when %r{\A(?<rel>(to[_-]?|this[_-]?)|(last[_-]?|yester[_-]?|next[_-]?)) (?<chunk>morrow|day|week|biweek|fortnight|semimonth|bimonth|month|quarter|half|year)\z}xi rel = Regexp.last_match[:rel] chunk = Regexp.last_match[:chunk].to_sym if chunk.match?(/morrow/i) chunk = :day rel = 'next' end if chunk.match?(/fortnight/i) chunk = :biweek end start = if rel.match?(/this|to/i) ::Date.today elsif rel.match?(/next/i) ::Date.today.add_chunk(chunk, 1) else # rel.match?(/last|yester/i) ::Date.today.add_chunk(chunk, -1) end if spec_type == :from start.beginning_of_chunk(chunk) else start.end_of_chunk(chunk) end when /^forever/i if spec_type == :from ::Date::BOT else ::Date::EOT end when /^never/i nil else raise ArgumentError, "XXX bad date spec: '#{spec}'" end # Now, skip to the next or preceding skip day if skip_to is defined. if skip_to wday = dow_to_wday(skip_to) if result.wday == wday && skip_dir[1] == '=' true # Do nothing elsif skip_dir[0] == '<' # Find prior date with dow result -= 1.day while result.wday != wday else # Find subsequent date with dow result += 1.day while result.wday != wday end end result end |