fat_core 5.1.0 → 5.3.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 +4 -4
- data/.rubocop.yml +5 -21
- data/Gemfile +14 -8
- data/README.org +170 -19
- data/Rakefile +10 -3
- data/bin/easters +22 -12
- data/fat_core.gemspec +2 -12
- data/lib/fat_core/date.rb +247 -258
- data/lib/fat_core/enumerable.rb +1 -1
- data/lib/fat_core/nil.rb +1 -1
- data/lib/fat_core/numeric.rb +19 -14
- data/lib/fat_core/range.rb +5 -5
- data/lib/fat_core/string.rb +37 -17
- data/lib/fat_core/symbol.rb +1 -1
- data/lib/fat_core/version.rb +1 -1
- data/spec/lib/date_spec.rb +172 -13
- data/spec/lib/enumerable_spec.rb +7 -4
- data/spec/lib/kernel_spec.rb +6 -6
- data/spec/lib/range_spec.rb +16 -14
- data/spec/lib/string_spec.rb +6 -11
- data/spec/spec_helper.rb +4 -0
- metadata +5 -131
- /data/spec/lib/{bigdecimal_spec.rb → big_decimal_spec.rb} +0 -0
- /data/spec/lib/{nil_spec.rb → nil_class_spec.rb} +0 -0
data/lib/fat_core/date.rb
CHANGED
@@ -9,49 +9,46 @@ require 'active_support/core_ext/integer/time'
|
|
9
9
|
require 'fat_core/string'
|
10
10
|
require 'fat_core/patches'
|
11
11
|
|
12
|
-
# ## FatCore Date Extensions
|
13
|
-
#
|
14
|
-
# The FatCore extensions to the Date class add the notion of several additional
|
15
|
-
# calendar periods besides years, months, and weeks to those provided for in the
|
16
|
-
# Date class and the active_support extensions to Date. In particular, there
|
17
|
-
# are several additional calendar subdivisions (called "chunks" in this
|
18
|
-
# documentation) supported by FatCore's extension to the Date class:
|
19
|
-
#
|
20
|
-
# * year,
|
21
|
-
# * half,
|
22
|
-
# * quarter,
|
23
|
-
# * bimonth,
|
24
|
-
# * month,
|
25
|
-
# * semimonth,
|
26
|
-
# * biweek,
|
27
|
-
# * week, and
|
28
|
-
# * day
|
29
|
-
#
|
30
|
-
# For each of those chunks, there are methods for finding the beginning and end
|
31
|
-
# of the chunk, for advancing or retreating a Date by the chunk, and for testing
|
32
|
-
# whether a Date is at the beginning or end of each of the chunk.
|
33
|
-
#
|
34
|
-
# FatCore's Date extension defines a few convenience formatting methods, such as
|
35
|
-
# Date#iso and Date#org for formatting Dates as ISO strings and as Emacs
|
36
|
-
# org-mode inactive timestamps respectively. It also has a few utility methods
|
37
|
-
# for determining the date of Easter, the number of days in any given month, and
|
38
|
-
# the Date of the nth workday in a given month (say the third Thursday in
|
39
|
-
# October, 2014).
|
40
|
-
#
|
41
|
-
# The Date extension defines a couple of class methods for parsing strings into
|
42
|
-
# Dates, especially Date.parse_spec, which allows Dates to be specified in a
|
43
|
-
# lazy way, either absolutely or relative to the computer's clock.
|
44
|
-
#
|
45
|
-
# Finally FatCore's Date extensions provide thorough methods for determining if
|
46
|
-
# a Date is a United States federal holiday or workday based on US law,
|
47
|
-
# including executive orders. It does the same for the New York Stock Exchange,
|
48
|
-
# based on the rules of the New York Stock Exchange, including dates on which
|
49
|
-
# the NYSE was closed for special reasons, such as the 9-11 attacks in 2001.
|
50
12
|
module FatCore
|
13
|
+
# ## FatCore Date Extensions
|
14
|
+
#
|
15
|
+
# The FatCore extensions to the Date class add the notion of several additional
|
16
|
+
# calendar periods besides years, months, and weeks to those provided for in the
|
17
|
+
# Date class and the active_support extensions to Date. In particular, there
|
18
|
+
# are several additional calendar subdivisions (called "chunks" in this
|
19
|
+
# documentation) supported by FatCore's extension to the Date class:
|
20
|
+
#
|
21
|
+
# * year,
|
22
|
+
# * half,
|
23
|
+
# * quarter,
|
24
|
+
# * bimonth,
|
25
|
+
# * month,
|
26
|
+
# * semimonth,
|
27
|
+
# * biweek,
|
28
|
+
# * week, and
|
29
|
+
# * day
|
30
|
+
#
|
31
|
+
# For each of those chunks, there are methods for finding the beginning and end
|
32
|
+
# of the chunk, for advancing or retreating a Date by the chunk, and for testing
|
33
|
+
# whether a Date is at the beginning or end of each of the chunk.
|
34
|
+
#
|
35
|
+
# FatCore's Date extension defines a few convenience formatting methods, such as
|
36
|
+
# Date#iso and Date#org for formatting Dates as ISO strings and as Emacs
|
37
|
+
# org-mode inactive timestamps respectively. It also has a few utility methods
|
38
|
+
# for determining the date of Easter, the number of days in any given month, and
|
39
|
+
# the Date of the nth workday in a given month (say the third Thursday in
|
40
|
+
# October, 2014).
|
41
|
+
#
|
42
|
+
# The Date extension defines a couple of class methods for parsing strings into
|
43
|
+
# Dates, especially Date.parse_spec, which allows Dates to be specified in a
|
44
|
+
# lazy way, either absolutely or relative to the computer's clock.
|
45
|
+
#
|
46
|
+
# Finally FatCore's Date extensions provide thorough methods for determining if
|
47
|
+
# a Date is a United States federal holiday or workday based on US law,
|
48
|
+
# including executive orders. It does the same for the New York Stock Exchange,
|
49
|
+
# based on the rules of the New York Stock Exchange, including dates on which
|
50
|
+
# the NYSE was closed for special reasons, such as the 9-11 attacks in 2001.
|
51
51
|
module Date
|
52
|
-
# Set the default beginning of week to Monday for commercial weeks.
|
53
|
-
# ::Date.beginning_of_week = :monday
|
54
|
-
|
55
52
|
# Constant for Beginning of Time (BOT) outside the range of what we would ever
|
56
53
|
# want to find in commercial situations.
|
57
54
|
BOT = ::Date.parse('1900-01-01')
|
@@ -369,7 +366,7 @@ module FatCore
|
|
369
366
|
# @param from_date [::Date] the middle of the six-month range
|
370
367
|
# @return [Boolean]
|
371
368
|
def within_6mos_of?(from_date)
|
372
|
-
from_date = Date.parse(from_date) unless from_date.is_a?(Date)
|
369
|
+
from_date = ::Date.parse(from_date) unless from_date.is_a?(Date)
|
373
370
|
from_day = from_date.day
|
374
371
|
if [28, 29, 30, 31].include?(from_day)
|
375
372
|
# Near the end of the month, we need to make sure that when we go
|
@@ -550,6 +547,8 @@ module FatCore
|
|
550
547
|
end
|
551
548
|
end
|
552
549
|
|
550
|
+
# NOTE: Date#end_of_week and Date#beginning_of_week is defined in ActiveSupport
|
551
|
+
|
553
552
|
# Return the date that is +n+ calendar halves after this date, where a
|
554
553
|
# calendar half is a period of 6 months.
|
555
554
|
#
|
@@ -653,8 +652,10 @@ module FatCore
|
|
653
652
|
if factor.positive?
|
654
653
|
[beginning_of_month + 16.days + (day - 1).days, end_of_month].min
|
655
654
|
else
|
656
|
-
[
|
657
|
-
|
655
|
+
[
|
656
|
+
prior_month.beginning_of_month + 16.days + (day - 1).days,
|
657
|
+
prior_month.end_of_month
|
658
|
+
].min
|
658
659
|
end
|
659
660
|
elsif factor.positive?
|
660
661
|
# In the second half of the month (17th to the 31st), go as many
|
@@ -1199,11 +1200,12 @@ module FatCore
|
|
1199
1200
|
# later trading day.
|
1200
1201
|
#
|
1201
1202
|
# @return [::Date]
|
1202
|
-
def
|
1203
|
+
def next_until_nyse_workday
|
1203
1204
|
date = dup
|
1204
1205
|
date += 1 until date.trading_day?
|
1205
1206
|
date
|
1206
1207
|
end
|
1208
|
+
alias_method :next_until_trading_day, :next_until_nyse_workday
|
1207
1209
|
|
1208
1210
|
# Return this date if its a trading day, otherwise skip back to the first prior
|
1209
1211
|
# trading day.
|
@@ -1330,7 +1332,7 @@ module FatCore
|
|
1330
1332
|
#
|
1331
1333
|
# @return [Boolean]
|
1332
1334
|
def nyse_special_holiday?
|
1333
|
-
return false
|
1335
|
+
return false if self <= ::Date.parse('1960-01-01')
|
1334
1336
|
|
1335
1337
|
return true if PRESIDENTIAL_FUNERALS.include?(self)
|
1336
1338
|
|
@@ -1427,6 +1429,7 @@ module FatCore
|
|
1427
1429
|
# `::Date.parse_spec` returns the first date in the period if `spec_type` is
|
1428
1430
|
# `:from` and the last date in the period if `spec_type` is `:to`:
|
1429
1431
|
#
|
1432
|
+
# * `YYYY-MM-DD` a particular date, so `:from` and `:to` return the same
|
1430
1433
|
# * `YYYY` is the whole year `YYYY`,
|
1431
1434
|
# * `YYYY-1H` or `YYYY-H1` is the first calendar half in year `YYYY`,
|
1432
1435
|
# * `H2` or `2H` is the second calendar half of the current year,
|
@@ -1437,8 +1440,19 @@ module FatCore
|
|
1437
1440
|
# * `4` or `04` is April in the current year,
|
1438
1441
|
# * `YYYY-W32` or `YYYY-32W` is the 32nd week in year YYYY,
|
1439
1442
|
# * `W32` or `32W` is the 32nd week in the current year,
|
1440
|
-
# * `
|
1441
|
-
#
|
1443
|
+
# * `W32-4` or `32W-4` is the 4th day of the 32nd week in the current year,
|
1444
|
+
# * `YYYY-MM-I` or `YYYY-MM-II` is the first or second half of the given month,
|
1445
|
+
# * `YYYY-MM-i` or `YYYY-MM-v` is the first or fifth week of the given month,
|
1446
|
+
# * `MM-i` or `MM-v` is the first or fifth week of the current month,
|
1447
|
+
# * `YYYY-MM-3Tu` or `YYYY-MM-4Mo` is the third Tuesdsay and fourth Monday of the given month,
|
1448
|
+
# * `MM-3Tu` or `MM-4Mo` is the third Tuesdsay and fourth Monday of the given month in the current year,
|
1449
|
+
# * `3Tu` or `4Mo` is the third Tuesdsay and fourth Monday of the current month,
|
1450
|
+
# * `YYYY-E` is Easter of the given year YYYY,
|
1451
|
+
# * `E` is Easter of the current year YYYY,
|
1452
|
+
# * `YYYY-E+50` and `YYYY-E-40` is 50 days after and 40 days before Easter of the given year,
|
1453
|
+
# * `E+50` and `E-40` is 50 days after and 40 days before Easter of the current year,
|
1454
|
+
# * `YYYY-001` and `YYYY-182` is first and 182nd day of the given year,
|
1455
|
+
# * `001` and `182` is first and 182nd day of the current year,
|
1442
1456
|
# * `this_<chunk>` where `<chunk>` is one of `year`, `half`, `quarter`,
|
1443
1457
|
# `bimonth`, `month`, `semimonth`, `biweek`, `week`, or `day`, the
|
1444
1458
|
# corresponding calendar period in which the current date falls,
|
@@ -1470,43 +1484,64 @@ module FatCore
|
|
1470
1484
|
# @return [::Date] date that is the first (:from) or last (:to) in the period
|
1471
1485
|
# designated by spec
|
1472
1486
|
def parse_spec(spec, spec_type = :from)
|
1473
|
-
spec = spec.to_s.strip
|
1487
|
+
spec = spec.to_s.strip.clean
|
1474
1488
|
unless %i[from to].include?(spec_type)
|
1475
1489
|
raise ArgumentError, "invalid date spec type: '#{spec_type}'"
|
1476
1490
|
end
|
1477
1491
|
|
1478
1492
|
today = ::Date.current
|
1479
|
-
case spec
|
1480
|
-
when %r{\A(?<yr>\d\d\d\d)[-/](?<
|
1481
|
-
#
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
raise ArgumentError, "invalid week number (1-53): '#{spec}'"
|
1493
|
+
case spec
|
1494
|
+
when %r{\A((?<yr>\d\d\d\d)[-/])?(?<doy>\d\d\d)\z}
|
1495
|
+
# With 3-digit YYYY-ddd, return the day-of-year
|
1496
|
+
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
1497
|
+
doy = Regexp.last_match[:doy].to_i
|
1498
|
+
max_doy = ::Date.gregorian_leap?(year) ? 366 : 365
|
1499
|
+
if doy > max_doy
|
1500
|
+
raise ArgumentError, "invalid day-of-year '#{doy}' (1..#{max_doy}) in '#{spec}'"
|
1488
1501
|
end
|
1489
1502
|
|
1490
|
-
|
1491
|
-
|
1503
|
+
::Date.new(year, 1, 1) + doy - 1
|
1504
|
+
when %r{\A((?<yr>\d\d\d\d)[-/])?(?<mo>\d\d?)([-/](?<dy>\d\d?))?\z}
|
1505
|
+
# MM, YYYY-MM, MM-DD
|
1506
|
+
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
1507
|
+
month = Regexp.last_match[:mo].to_i
|
1508
|
+
day = Regexp.last_match[:dy]&.to_i
|
1509
|
+
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
1510
|
+
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
1511
|
+
end
|
1512
|
+
|
1513
|
+
if day
|
1514
|
+
::Date.new(year, month, day)
|
1515
|
+
elsif spec_type == :from
|
1516
|
+
::Date.new(year, month, 1)
|
1492
1517
|
else
|
1493
|
-
::Date.
|
1518
|
+
::Date.new(year, month, 1).end_of_month
|
1494
1519
|
end
|
1495
|
-
when %r{\A(?<yr>\d\d\d\d)[-/]
|
1496
|
-
|
1520
|
+
when %r{\A((?<yr>\d\d\d\d)[-/])?(?<wk>\d\d?)W(-(?<dy>\d))?\z}xi,
|
1521
|
+
%r{\A((?<yr>\d\d\d\d)[-/])?W(?<wk>\d\d?)(-(?<dy>\d))?\z}xi
|
1522
|
+
# Commercial week numbers. The first commercial week of the year is
|
1523
|
+
# the one that includes the first Thursday of that year. In the
|
1524
|
+
# Gregorian calendar, this is equivalent to the week which includes
|
1525
|
+
# January 4. This appears to be the equivalent of ISO 8601 week
|
1526
|
+
# number as described at https://en.wikipedia.org/wiki/ISO_week_date
|
1527
|
+
year = Regexp.last_match[:yr]&.to_i
|
1497
1528
|
week_num = Regexp.last_match[:wk].to_i
|
1498
|
-
|
1529
|
+
day = Regexp.last_match[:dy]&.to_i
|
1530
|
+
unless (1..53).cover?(week_num)
|
1499
1531
|
raise ArgumentError, "invalid week number (1-53): '#{spec}'"
|
1500
1532
|
end
|
1533
|
+
if day && !(1..7).cover?(day)
|
1534
|
+
raise ArgumentError, "invalid ISO day number (1-7): '#{spec}'"
|
1535
|
+
end
|
1501
1536
|
|
1502
1537
|
if spec_type == :from
|
1503
|
-
::Date.commercial(year, week_num)
|
1538
|
+
::Date.commercial(year ? year : today.year, week_num, day ? day : 1)
|
1504
1539
|
else
|
1505
|
-
::Date.commercial(year, week_num)
|
1540
|
+
::Date.commercial(year ? year : today.year, week_num, day ? day : 7)
|
1506
1541
|
end
|
1507
|
-
when %r{^(?<yr>\d\d\d\d)[-/](?<qt>\d)[Qq]$}, %r{^(?<yr>\d\d\d\d)[-/][Qq](?<qt>\d)$}
|
1542
|
+
when %r{^((?<yr>\d\d\d\d)[-/])?(?<qt>\d)[Qq]$}, %r{^((?<yr>\d\d\d\d)[-/])?[Qq](?<qt>\d)$}
|
1508
1543
|
# Year-Quarter
|
1509
|
-
year = Regexp.last_match[:yr].
|
1544
|
+
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
1510
1545
|
quarter = Regexp.last_match[:qt].to_i
|
1511
1546
|
unless [1, 2, 3, 4].include?(quarter)
|
1512
1547
|
raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
|
@@ -1518,26 +1553,12 @@ module FatCore
|
|
1518
1553
|
else
|
1519
1554
|
::Date.new(year, month, 1).end_of_quarter
|
1520
1555
|
end
|
1521
|
-
when
|
1522
|
-
# Quarter only
|
1523
|
-
this_year = today.year
|
1524
|
-
quarter = Regexp.last_match[:qt].to_i
|
1525
|
-
unless [1, 2, 3, 4].include?(quarter)
|
1526
|
-
raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
|
1527
|
-
end
|
1528
|
-
|
1529
|
-
date = ::Date.new(this_year, quarter * 3, 15)
|
1530
|
-
if spec_type == :from
|
1531
|
-
date.beginning_of_quarter
|
1532
|
-
else
|
1533
|
-
date.end_of_quarter
|
1534
|
-
end
|
1535
|
-
when %r{^(?<yr>\d\d\d\d)[-/](?<hf>\d)[Hh]$}, %r{^(?<yr>\d\d\d\d)[-/][Hh](?<hf>\d)$}
|
1556
|
+
when %r{^((?<yr>\d\d\d\d)[-/])?(?<hf>\d)[Hh]$}, %r{^((?<yr>\d\d\d\d)[-/])?[Hh](?<hf>\d)$}
|
1536
1557
|
# Year-Half
|
1537
|
-
year = Regexp.last_match[:yr].
|
1558
|
+
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
1538
1559
|
half = Regexp.last_match[:hf].to_i
|
1539
1560
|
msg = "invalid half number: '#{spec}'"
|
1540
|
-
raise ArgumentError,
|
1561
|
+
raise ArgumentError, msg unless [1, 2].include?(half)
|
1541
1562
|
|
1542
1563
|
month = half * 6
|
1543
1564
|
if spec_type == :from
|
@@ -1545,58 +1566,7 @@ module FatCore
|
|
1545
1566
|
else
|
1546
1567
|
::Date.new(year, month, 1).end_of_half
|
1547
1568
|
end
|
1548
|
-
when
|
1549
|
-
# Half only
|
1550
|
-
this_year = today.year
|
1551
|
-
half = Regexp.last_match[:hf].to_i
|
1552
|
-
msg = "invalid half number: '#{spec}'"
|
1553
|
-
raise ArgumentError, msg unless [1, 2].include?(half)
|
1554
|
-
|
1555
|
-
date = ::Date.new(this_year, half * 6, 15)
|
1556
|
-
if spec_type == :from
|
1557
|
-
date.beginning_of_half
|
1558
|
-
else
|
1559
|
-
date.end_of_half
|
1560
|
-
end
|
1561
|
-
when %r{^(?<yr>\d\d\d\d)[-/](?<mo>\d\d?)*$}
|
1562
|
-
# Year-Month only
|
1563
|
-
year = Regexp.last_match[:yr].to_i
|
1564
|
-
month = Regexp.last_match[:mo].to_i
|
1565
|
-
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
1566
|
-
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
1567
|
-
end
|
1568
|
-
|
1569
|
-
if spec_type == :from
|
1570
|
-
::Date.new(year, month, 1)
|
1571
|
-
else
|
1572
|
-
::Date.new(year, month, 1).end_of_month
|
1573
|
-
end
|
1574
|
-
when %r{^(?<mo>\d\d?)[-/](?<dy>\d\d?)*$}
|
1575
|
-
# Month-Day only
|
1576
|
-
month = Regexp.last_match[:mo].to_i
|
1577
|
-
day = Regexp.last_match[:dy].to_i
|
1578
|
-
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
1579
|
-
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
1580
|
-
end
|
1581
|
-
|
1582
|
-
if spec_type == :from
|
1583
|
-
::Date.new(today.year, month, day)
|
1584
|
-
else
|
1585
|
-
::Date.new(today.year, month, day).end_of_month
|
1586
|
-
end
|
1587
|
-
when /\A(?<mo>\d\d?)\z/
|
1588
|
-
# Month only
|
1589
|
-
month = Regexp.last_match[:mo].to_i
|
1590
|
-
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
1591
|
-
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
1592
|
-
end
|
1593
|
-
|
1594
|
-
if spec_type == :from
|
1595
|
-
::Date.new(today.year, month, 1)
|
1596
|
-
else
|
1597
|
-
::Date.new(today.year, month, 1).end_of_month
|
1598
|
-
end
|
1599
|
-
when /^(?<yr>\d\d\d\d)$/
|
1569
|
+
when /\A(?<yr>\d\d\d\d)\z/
|
1600
1570
|
# Year only
|
1601
1571
|
year = Regexp.last_match[:yr].to_i
|
1602
1572
|
if spec_type == :from
|
@@ -1604,105 +1574,88 @@ module FatCore
|
|
1604
1574
|
else
|
1605
1575
|
::Date.new(year, 12, 31)
|
1606
1576
|
end
|
1607
|
-
when
|
1608
|
-
|
1609
|
-
|
1610
|
-
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
if spec_type == :from
|
1615
|
-
(today - 1.week).beginning_of_week
|
1616
|
-
else
|
1617
|
-
(today - 1.week).end_of_week
|
1618
|
-
end
|
1619
|
-
when /^(this_?)?biweek/
|
1620
|
-
if spec_type == :from
|
1621
|
-
today.beginning_of_biweek
|
1622
|
-
else
|
1623
|
-
today.end_of_biweek
|
1624
|
-
end
|
1625
|
-
when /last_?biweek/
|
1626
|
-
if spec_type == :from
|
1627
|
-
(today - 2.week).beginning_of_biweek
|
1628
|
-
else
|
1629
|
-
(today - 2.week).end_of_biweek
|
1630
|
-
end
|
1631
|
-
when /^(this_?)?semimonth/
|
1632
|
-
spec_type == :from ? today.beginning_of_semimonth : today.end_of_semimonth
|
1633
|
-
when /^last_?semimonth/
|
1634
|
-
if spec_type == :from
|
1635
|
-
(today - 15.days).beginning_of_semimonth
|
1636
|
-
else
|
1637
|
-
(today - 15.days).end_of_semimonth
|
1638
|
-
end
|
1639
|
-
when /^(this_?)?month/
|
1640
|
-
if spec_type == :from
|
1641
|
-
today.beginning_of_month
|
1642
|
-
else
|
1643
|
-
today.end_of_month
|
1644
|
-
end
|
1645
|
-
when /^last_?month/
|
1646
|
-
if spec_type == :from
|
1647
|
-
(today - 1.month).beginning_of_month
|
1648
|
-
else
|
1649
|
-
(today - 1.month).end_of_month
|
1650
|
-
end
|
1651
|
-
when /^(this_?)?bimonth/
|
1652
|
-
if spec_type == :from
|
1653
|
-
today.beginning_of_bimonth
|
1654
|
-
else
|
1655
|
-
today.end_of_bimonth
|
1656
|
-
end
|
1657
|
-
when /^last_?bimonth/
|
1658
|
-
if spec_type == :from
|
1659
|
-
(today - 2.month).beginning_of_bimonth
|
1660
|
-
else
|
1661
|
-
(today - 2.month).end_of_bimonth
|
1662
|
-
end
|
1663
|
-
when /^(this_?)?quarter/
|
1664
|
-
if spec_type == :from
|
1665
|
-
today.beginning_of_quarter
|
1577
|
+
when %r{\A(?<yr>\d\d\d\d)[-/](?<mo>\d\d?)[-/](?<hf_mo>(I|II))\z}
|
1578
|
+
# Year, month, half-month, designated with uppercase Roman
|
1579
|
+
year = Regexp.last_match[:yr].to_i
|
1580
|
+
month = Regexp.last_match[:mo].to_i
|
1581
|
+
hf_mo = Regexp.last_match[:hf_mo]
|
1582
|
+
if hf_mo == "I"
|
1583
|
+
spec_type == :from ? ::Date.new(year, month, 1) : ::Date.new(year, month, 15)
|
1666
1584
|
else
|
1667
|
-
|
1585
|
+
# hf_mo == "II"
|
1586
|
+
spec_type == :from ? ::Date.new(year, month, 16) : ::Date.new(year, month, 16).end_of_month
|
1668
1587
|
end
|
1669
|
-
when
|
1670
|
-
|
1671
|
-
|
1588
|
+
when %r{\A((?<yr>\d\d\d\d)[-/])?((?<mo>\d\d?)[-/])?(?<wk>(i|ii|iii|iv|v|vi))\z}
|
1589
|
+
# Year, month, week-of-month, partial-or-whole, designated with lowercase Roman
|
1590
|
+
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
1591
|
+
month = Regexp.last_match[:mo]&.to_i || ::Date.today.month
|
1592
|
+
wk = ['i', 'ii', 'iii', 'iv', 'v', 'vi'].index(Regexp.last_match[:wk]) + 1
|
1593
|
+
result =
|
1594
|
+
if spec_type == :from
|
1595
|
+
::Date.new(year, month, 1).beginning_of_week + (wk - 1).weeks
|
1596
|
+
else
|
1597
|
+
::Date.new(year, month, 1).end_of_week + (wk - 1).weeks
|
1598
|
+
end
|
1599
|
+
# If beginning of week of the 1st is in prior month, return the 1st
|
1600
|
+
result = [result, ::Date.new(year, month, 1)].max
|
1601
|
+
# If the whole week of the result is in the next month, there was no such week
|
1602
|
+
if result.beginning_of_week.month > month
|
1603
|
+
msg = sprintf("no week number #{wk} in %04d-%02d", year, month)
|
1604
|
+
raise ArgumentError, msg
|
1672
1605
|
else
|
1673
|
-
|
1606
|
+
# But if part of the result week is in this month, return end of month
|
1607
|
+
[result, ::Date.new(year, month, 1).end_of_month].min
|
1674
1608
|
end
|
1675
|
-
when
|
1676
|
-
|
1677
|
-
|
1678
|
-
|
1679
|
-
|
1609
|
+
when %r{\A((?<yr>\d\d\d\d)[-/])?((?<mo>\d\d?)[-/])?((?<ndow>\d+)(?<dow>Su|Mo|Tu|We|Th|Fr|Sa))\z}
|
1610
|
+
# Year, month, week-of-month, partial-or-whole, designated with lowercase Roman
|
1611
|
+
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
1612
|
+
month = Regexp.last_match[:mo]&.to_i || ::Date.today.month
|
1613
|
+
ndow = Regexp.last_match[:ndow].to_i
|
1614
|
+
dow = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'].index(Regexp.last_match[:dow]) ||
|
1615
|
+
['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'].index(Regexp.last_match[:dow])
|
1616
|
+
unless (1..12).cover?(month)
|
1617
|
+
raise ArgumentError, "invalid month number (1-12): '#{month}' in '#{spec}'"
|
1680
1618
|
end
|
1681
|
-
|
1682
|
-
|
1683
|
-
(today - 6.months).beginning_of_half
|
1684
|
-
else
|
1685
|
-
(today - 6.months).end_of_half
|
1619
|
+
unless (1..5).cover?(ndow)
|
1620
|
+
raise ArgumentError, "invalid ordinal day number (1-5): '#{ndow}' in '#{spec}'"
|
1686
1621
|
end
|
1687
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1691
|
-
|
1622
|
+
|
1623
|
+
::Date.nth_wday_in_year_month(ndow, dow, year, month)
|
1624
|
+
when %r{\A(?<yr>\d\d\d\d[-/])?E(?<off>[+-]\d+)?\z}i
|
1625
|
+
# Easter for the given year, current year (if no year component),
|
1626
|
+
# optionally plus or minus a day offset
|
1627
|
+
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
1628
|
+
offset = Regexp.last_match[:off]&.to_i || 0
|
1629
|
+
::Date.easter(year) + offset
|
1630
|
+
when %r{\A(?<rel>(to[_-]?|this[_-]?)|(last[_-]?|yester[_-]?|next[_-]?))
|
1631
|
+
(?<chunk>morrow|day|week|biweek|semimonth|bimonth|month|quarter|half|year)\z}xi
|
1632
|
+
rel = Regexp.last_match[:rel]
|
1633
|
+
chunk = Regexp.last_match[:chunk].to_sym
|
1634
|
+
if chunk.match?(/morrow/i)
|
1635
|
+
chunk = :day
|
1636
|
+
rel = 'next'
|
1692
1637
|
end
|
1693
|
-
|
1638
|
+
start =
|
1639
|
+
if rel.match?(/this|to/i)
|
1640
|
+
::Date.today
|
1641
|
+
elsif rel.match?(/next/i)
|
1642
|
+
::Date.today.add_chunk(chunk, 1)
|
1643
|
+
else
|
1644
|
+
# rel.match?(/last|yester/i)
|
1645
|
+
::Date.today.add_chunk(chunk, -1)
|
1646
|
+
end
|
1694
1647
|
if spec_type == :from
|
1695
|
-
(
|
1648
|
+
start.beginning_of_chunk(chunk)
|
1696
1649
|
else
|
1697
|
-
(
|
1650
|
+
start.end_of_chunk(chunk)
|
1698
1651
|
end
|
1699
|
-
when /^forever/
|
1652
|
+
when /^forever/i
|
1700
1653
|
if spec_type == :from
|
1701
1654
|
::Date::BOT
|
1702
1655
|
else
|
1703
1656
|
::Date::EOT
|
1704
1657
|
end
|
1705
|
-
when /^never/
|
1658
|
+
when /^never/i
|
1706
1659
|
nil
|
1707
1660
|
else
|
1708
1661
|
raise ArgumentError, "bad date spec: '#{spec}''"
|
@@ -1713,8 +1666,21 @@ module FatCore
|
|
1713
1666
|
|
1714
1667
|
# An Array of the number of days in each month indexed by month number,
|
1715
1668
|
# starting with January = 1, etc.
|
1716
|
-
COMMON_YEAR_DAYS_IN_MONTH = [
|
1717
|
-
|
1669
|
+
COMMON_YEAR_DAYS_IN_MONTH = [
|
1670
|
+
31,
|
1671
|
+
31,
|
1672
|
+
28,
|
1673
|
+
31,
|
1674
|
+
30,
|
1675
|
+
31,
|
1676
|
+
30,
|
1677
|
+
31,
|
1678
|
+
31,
|
1679
|
+
30,
|
1680
|
+
31,
|
1681
|
+
30,
|
1682
|
+
31
|
1683
|
+
].freeze
|
1718
1684
|
def days_in_month(year, month)
|
1719
1685
|
raise ArgumentError, 'illegal month number' if month < 1 || month > 12
|
1720
1686
|
|
@@ -1758,8 +1724,6 @@ module FatCore
|
|
1758
1724
|
11
|
1759
1725
|
when /\Adec/i
|
1760
1726
|
12
|
1761
|
-
else
|
1762
|
-
nil
|
1763
1727
|
end
|
1764
1728
|
end
|
1765
1729
|
|
@@ -1767,7 +1731,7 @@ module FatCore
|
|
1767
1731
|
# last day of month.
|
1768
1732
|
#
|
1769
1733
|
# @param nth [Integer] the ordinal number for the weekday
|
1770
|
-
# @param wday [Integer] the weekday of interest with
|
1734
|
+
# @param wday [Integer] the weekday of interest with Sunday 0 to Saturday 6
|
1771
1735
|
# @param year [Integer] the year of interest
|
1772
1736
|
# @param month [Integer] the month of interest with January 1 to December 12
|
1773
1737
|
def nth_wday_in_year_month(nth, wday, year, month)
|
@@ -1778,32 +1742,44 @@ module FatCore
|
|
1778
1742
|
raise ArgumentError, 'illegal month number' if month < 1 || month > 12
|
1779
1743
|
|
1780
1744
|
nth = nth.to_i
|
1781
|
-
if nth.
|
1782
|
-
#
|
1783
|
-
|
1784
|
-
|
1785
|
-
|
1786
|
-
|
1787
|
-
|
1788
|
-
d
|
1789
|
-
|
1790
|
-
|
1791
|
-
|
1792
|
-
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
1797
|
-
|
1798
|
-
|
1799
|
-
|
1800
|
-
d
|
1801
|
-
|
1745
|
+
if nth.abs > 5
|
1746
|
+
raise ArgumentError, "#{nth.abs} out of range: can be no more than 5 of any day of the week in any month"
|
1747
|
+
end
|
1748
|
+
|
1749
|
+
result =
|
1750
|
+
if nth.positive?
|
1751
|
+
# Set d to the 1st wday in month
|
1752
|
+
d = ::Date.new(year, month, 1)
|
1753
|
+
d += 1 while d.wday != wday
|
1754
|
+
# Set d to the nth wday in month
|
1755
|
+
nd = 1
|
1756
|
+
while nd != nth
|
1757
|
+
d += 7
|
1758
|
+
nd += 1
|
1759
|
+
end
|
1760
|
+
d
|
1761
|
+
elsif nth.negative?
|
1762
|
+
nth = -nth
|
1763
|
+
# Set d to the last wday in month
|
1764
|
+
d = ::Date.new(year, month, 1).end_of_month
|
1765
|
+
d -= 1 while d.wday != wday
|
1766
|
+
# Set d to the nth wday in month
|
1767
|
+
nd = 1
|
1768
|
+
while nd != nth
|
1769
|
+
d -= 7
|
1770
|
+
nd += 1
|
1771
|
+
end
|
1772
|
+
d
|
1773
|
+
else
|
1774
|
+
raise ArgumentError, 'Argument nth cannot be zero'
|
1802
1775
|
end
|
1803
|
-
|
1804
|
-
|
1805
|
-
|
1776
|
+
|
1777
|
+
dow = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][wday]
|
1778
|
+
if result.month != month
|
1779
|
+
raise ArgumentError, "There is no #{nth}th #{dow} in #{year}-#{month}"
|
1806
1780
|
end
|
1781
|
+
|
1782
|
+
result
|
1807
1783
|
end
|
1808
1784
|
|
1809
1785
|
# Return the date of Easter for the Western Church in the given year.
|
@@ -1826,23 +1802,33 @@ module FatCore
|
|
1826
1802
|
end
|
1827
1803
|
|
1828
1804
|
# Ensure that date is of class Date based either on a string or Date
|
1829
|
-
# object.
|
1805
|
+
# object or an object that responds to #to_date or #to_datetime. If the
|
1806
|
+
# given object is a String, use Date.parse to try to convert it.
|
1807
|
+
#
|
1808
|
+
# If given a DateTime, it returns the unrounded DateTime; if given a
|
1809
|
+
# Time, it converts it to a DateTime.
|
1830
1810
|
#
|
1831
1811
|
# @param dat [String, Date, Time] the object to be converted to Date
|
1832
1812
|
# @return [Date, DateTime]
|
1833
1813
|
def ensure_date(dat)
|
1834
|
-
|
1835
|
-
when String
|
1836
|
-
::Date.parse(dat)
|
1837
|
-
when Date, DateTime
|
1814
|
+
if dat.is_a?(Date) || dat.is_a?(DateTime)
|
1838
1815
|
dat
|
1839
|
-
|
1816
|
+
elsif dat.is_a?(Time)
|
1817
|
+
dat.to_datetime
|
1818
|
+
elsif dat.respond_to?(:to_datetime)
|
1819
|
+
dat.to_datetime
|
1820
|
+
elsif dat.respond_to?(:to_date)
|
1840
1821
|
dat.to_date
|
1822
|
+
elsif dat.is_a?(String)
|
1823
|
+
begin
|
1824
|
+
::Date.parse(dat)
|
1825
|
+
rescue ::Date::Error
|
1826
|
+
raise ArgumentError, "cannot convert string '#{dat}' to a Date or DateTime"
|
1827
|
+
end
|
1841
1828
|
else
|
1842
|
-
raise ArgumentError, '
|
1829
|
+
raise ArgumentError, "cannot convert class '#{dat.class}' to a Date or DateTime"
|
1843
1830
|
end
|
1844
1831
|
end
|
1845
|
-
alias ensure ensure_date
|
1846
1832
|
end
|
1847
1833
|
|
1848
1834
|
def self.included(base)
|
@@ -1853,6 +1839,9 @@ end
|
|
1853
1839
|
|
1854
1840
|
class Date
|
1855
1841
|
include FatCore::Date
|
1842
|
+
def self.ensure(dat)
|
1843
|
+
ensure_date(dat)
|
1844
|
+
end
|
1856
1845
|
# @!parse include FatCore::Date
|
1857
1846
|
# @!parse extend FatCore::Date::ClassMethods
|
1858
1847
|
end
|