jw_calendar 0.1.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/CODE_OF_CONDUCT.md +17 -0
- data/CONTRIBUTING.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +214 -0
- data/SECURITY.md +11 -0
- data/docs/architecture.md +26 -0
- data/docs/civil-dates.md +14 -0
- data/docs/cli.md +14 -0
- data/docs/correctness.md +7 -0
- data/docs/gregorian.md +9 -0
- data/docs/iso-week.md +12 -0
- data/docs/julian-day-number.md +9 -0
- data/docs/julian.md +13 -0
- data/docs/month-grid.md +7 -0
- data/docs/reform-calendars.md +14 -0
- data/exe/jwcalendar +8 -0
- data/lib/jw_calendar/arithmetic/floor_division.rb +19 -0
- data/lib/jw_calendar/boundary/analyzer.rb +147 -0
- data/lib/jw_calendar/boundary/report.rb +28 -0
- data/lib/jw_calendar/calendars/gregorian.rb +114 -0
- data/lib/jw_calendar/calendars/julian.rb +90 -0
- data/lib/jw_calendar/calendars/reform_calendar.rb +104 -0
- data/lib/jw_calendar/civil_date.rb +135 -0
- data/lib/jw_calendar/cli/runner.rb +196 -0
- data/lib/jw_calendar/conversion/calendar_converter.rb +22 -0
- data/lib/jw_calendar/conversion/julian_day_number.rb +75 -0
- data/lib/jw_calendar/conversion/ordinal_date.rb +52 -0
- data/lib/jw_calendar/errors.rb +9 -0
- data/lib/jw_calendar/formatting/iso8601.rb +20 -0
- data/lib/jw_calendar/grid/cell.rb +26 -0
- data/lib/jw_calendar/grid/month_grid.rb +114 -0
- data/lib/jw_calendar/iso/week_date.rb +80 -0
- data/lib/jw_calendar/range/date_range.rb +83 -0
- data/lib/jw_calendar/version.rb +5 -0
- data/lib/jw_calendar.rb +23 -0
- metadata +86 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWCalendar
|
|
4
|
+
module Conversion
|
|
5
|
+
# Integer JDN and exact JD/MJD conversions. A civil date starts at midnight;
|
|
6
|
+
# its JDN is the integer labelled at the following astronomical noon.
|
|
7
|
+
module JulianDayNumber
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
HALF = Rational(1, 2)
|
|
11
|
+
MJD_OFFSET = Rational(2_400_001, 1)
|
|
12
|
+
|
|
13
|
+
# Return the date's integer JDN.
|
|
14
|
+
def for(date)
|
|
15
|
+
date.to_jdn
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Convert an integer JDN into a civil date.
|
|
19
|
+
def to_date(jdn, calendar: :gregorian)
|
|
20
|
+
CivilDate.from_jdn(jdn, calendar:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Return exact Julian Date for an explicitly supplied fraction of a day.
|
|
24
|
+
# Return JD at midnight plus an optional exact fraction of a day.
|
|
25
|
+
def jd(date, fraction: 0)
|
|
26
|
+
fraction = rational_fraction!(fraction)
|
|
27
|
+
raise ArgumentError, "fraction must be in 0...1" unless fraction >= 0 && fraction < 1
|
|
28
|
+
|
|
29
|
+
Rational(date.to_jdn, 1) - HALF + fraction
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Convert exact JD to [civil_date, fraction_of_day] at midnight boundaries.
|
|
33
|
+
# Split a JD into a civil date and exact fraction since midnight.
|
|
34
|
+
def from_jd(value, calendar: :gregorian)
|
|
35
|
+
jd_value = rational_number!(value)
|
|
36
|
+
shifted = jd_value + HALF
|
|
37
|
+
jdn = shifted.floor
|
|
38
|
+
[to_date(jdn, calendar:), shifted - jdn]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return MJD at midnight plus an optional exact fraction of a day.
|
|
42
|
+
def mjd(date, fraction: 0)
|
|
43
|
+
fraction = rational_fraction!(fraction)
|
|
44
|
+
raise ArgumentError, "fraction must be in 0...1" unless fraction >= 0 && fraction < 1
|
|
45
|
+
|
|
46
|
+
Rational(date.to_jdn, 1) - MJD_OFFSET + fraction
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Split an MJD into a civil date and exact fraction since midnight.
|
|
50
|
+
def from_mjd(value, calendar: :gregorian)
|
|
51
|
+
mjd_value = rational_number!(value)
|
|
52
|
+
day_offset = mjd_value.floor
|
|
53
|
+
fraction = mjd_value - day_offset
|
|
54
|
+
# MJD 0 is Gregorian 1858-11-17 at midnight (JDN 2,400,001).
|
|
55
|
+
[to_date(day_offset + 2_400_001, calendar:), fraction]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def rational_fraction!(value)
|
|
59
|
+
raise ArgumentError, "fraction must be Numeric" unless value.is_a?(Numeric)
|
|
60
|
+
|
|
61
|
+
value.to_r
|
|
62
|
+
end
|
|
63
|
+
private_class_method :rational_fraction!
|
|
64
|
+
|
|
65
|
+
def rational_number!(value)
|
|
66
|
+
raise ArgumentError, "Julian date value must be Numeric" unless value.is_a?(Numeric)
|
|
67
|
+
|
|
68
|
+
value.to_r
|
|
69
|
+
rescue RangeError
|
|
70
|
+
raise ArgumentError, "Julian date value must be finite"
|
|
71
|
+
end
|
|
72
|
+
private_class_method :rational_number!
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWCalendar
|
|
4
|
+
module Conversion
|
|
5
|
+
# Immutable year/day-of-year value, using the selected civil calendar.
|
|
6
|
+
class OrdinalDate
|
|
7
|
+
attr_reader :year, :day, :calendar
|
|
8
|
+
|
|
9
|
+
# Create a validated year and ordinal day.
|
|
10
|
+
def initialize(year, day, calendar: :gregorian)
|
|
11
|
+
unless CivilDate::CALENDARS.include?(calendar)
|
|
12
|
+
raise InvalidCalendarError, "calendar must be :gregorian or :julian"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
engine = calendar == :gregorian ? Calendars::Gregorian : Calendars::Julian
|
|
16
|
+
engine.days_in_year(year)
|
|
17
|
+
unless day.is_a?(Integer) && day.between?(1, engine.days_in_year(year))
|
|
18
|
+
raise InvalidDateError, "ordinal day must be in 1..#{engine.days_in_year(year)} for #{year}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@year = year
|
|
22
|
+
@day = day
|
|
23
|
+
@calendar = calendar
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Convert a CivilDate to its ordinal form.
|
|
28
|
+
def self.for(date)
|
|
29
|
+
new(date.year, date.ordinal_day, calendar: date.calendar)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Convert the ordinal value to its calendar date.
|
|
33
|
+
def to_date
|
|
34
|
+
jan1 = CivilDate.new(year, 1, 1, calendar:)
|
|
35
|
+
jan1.add_days(day - 1)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_s
|
|
39
|
+
format("%<year>04d-%<day>03d", year:, day:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ==(other)
|
|
43
|
+
other.is_a?(OrdinalDate) && [year, day, calendar] == [other.year, other.day, other.calendar]
|
|
44
|
+
end
|
|
45
|
+
alias eql? ==
|
|
46
|
+
|
|
47
|
+
def hash
|
|
48
|
+
[year, day, calendar].hash
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWCalendar
|
|
4
|
+
module Formatting
|
|
5
|
+
# Strict parsing/formatting helpers for supported extended ISO civil dates.
|
|
6
|
+
module ISO8601
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def parse_date(value, calendar: :gregorian)
|
|
10
|
+
CivilDate.parse(value, calendar:)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def format_date(date)
|
|
14
|
+
raise ArgumentError, "date must be a CivilDate" unless date.is_a?(CivilDate)
|
|
15
|
+
|
|
16
|
+
date.to_s
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWCalendar
|
|
4
|
+
module Grid
|
|
5
|
+
# Immutable cell in a month grid. A nil date represents a blank spillover.
|
|
6
|
+
class Cell
|
|
7
|
+
attr_reader :date, :week_index, :column_index
|
|
8
|
+
|
|
9
|
+
def initialize(date:, in_current_month:, week_index:, column_index:)
|
|
10
|
+
@date = date
|
|
11
|
+
@in_current_month = in_current_month
|
|
12
|
+
@week_index = week_index
|
|
13
|
+
@column_index = column_index
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def in_current_month?
|
|
18
|
+
@in_current_month
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def weekday
|
|
22
|
+
date&.weekday
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWCalendar
|
|
4
|
+
module Grid
|
|
5
|
+
# Deterministic structured month layout. Weeks may be natural length (4–6)
|
|
6
|
+
# or fixed to six rows for print and stable UI layouts.
|
|
7
|
+
class MonthGrid
|
|
8
|
+
WEEKDAY_STARTS = { monday: 1, sunday: 7 }.freeze
|
|
9
|
+
attr_reader :year, :month, :week_start, :fixed_weeks, :calendar
|
|
10
|
+
|
|
11
|
+
# Build a month grid without producing any HTML or locale-dependent output.
|
|
12
|
+
def initialize(year:, month:, week_start: :monday, fixed_weeks: nil,
|
|
13
|
+
include_adjacent: true, calendar: :gregorian)
|
|
14
|
+
engine = calendar_engine(calendar)
|
|
15
|
+
engine.days_in_month(year, month)
|
|
16
|
+
week_start = normalize_week_start(week_start)
|
|
17
|
+
validate_fixed_weeks!(fixed_weeks)
|
|
18
|
+
|
|
19
|
+
@year = year
|
|
20
|
+
@month = month
|
|
21
|
+
@week_start = week_start
|
|
22
|
+
@fixed_weeks = fixed_weeks
|
|
23
|
+
@calendar = calendar
|
|
24
|
+
@include_adjacent = include_adjacent
|
|
25
|
+
first = CivilDate.new(year, month, 1, calendar:)
|
|
26
|
+
leading, weeks = layout_dimensions(first, engine, week_start, fixed_weeks)
|
|
27
|
+
@rows = build_rows(first, leading, weeks)
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Return seven-cell rows.
|
|
32
|
+
attr_reader :rows
|
|
33
|
+
|
|
34
|
+
def weeks
|
|
35
|
+
rows.length
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cells
|
|
39
|
+
rows.flatten.freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Return column headings in display order.
|
|
43
|
+
def day_names
|
|
44
|
+
names = ISO::WeekDate::WEEKDAY_NAMES
|
|
45
|
+
Array.new(7) { |index| names[(week_start - 1 + index) % 7] }.freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_a
|
|
49
|
+
rows.map { |row| row.map { |cell| cell.date&.day } }.freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return a JSON-friendly hash of grid semantics.
|
|
53
|
+
def to_h
|
|
54
|
+
{
|
|
55
|
+
year:, month:, calendar:, week_start:,
|
|
56
|
+
fixed_weeks:, weekdays: day_names,
|
|
57
|
+
rows: rows.map do |row|
|
|
58
|
+
row.map do |cell|
|
|
59
|
+
{ date: cell.date&.to_s, day: cell.date&.day,
|
|
60
|
+
in_current_month: cell.in_current_month?, weekday: cell.weekday,
|
|
61
|
+
week_index: cell.week_index, column_index: cell.column_index }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def calendar_engine(calendar)
|
|
70
|
+
return Calendars::Gregorian if calendar == :gregorian
|
|
71
|
+
return Calendars::Julian if calendar == :julian
|
|
72
|
+
|
|
73
|
+
raise InvalidCalendarError, "calendar must be :gregorian or :julian"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normalize_week_start(week_start)
|
|
77
|
+
return week_start if week_start.is_a?(Integer) && week_start.between?(1, 7)
|
|
78
|
+
|
|
79
|
+
normalized = WEEKDAY_STARTS[week_start]
|
|
80
|
+
raise ArgumentError, "week_start must be :monday, :sunday, or ISO weekday 1..7" unless normalized
|
|
81
|
+
|
|
82
|
+
normalized
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_fixed_weeks!(fixed_weeks)
|
|
86
|
+
return if fixed_weeks.nil? || [4, 5, 6].include?(fixed_weeks)
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "fixed_weeks must be nil, 4, 5, or 6"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def layout_dimensions(first, engine, week_start, fixed_weeks)
|
|
92
|
+
leading = (first.weekday - week_start) % 7
|
|
93
|
+
natural_weeks = (leading + engine.days_in_month(year, month) + 6) / 7
|
|
94
|
+
weeks = fixed_weeks || natural_weeks
|
|
95
|
+
if weeks < natural_weeks
|
|
96
|
+
raise ArgumentError, "#{weeks} rows cannot contain all dates for #{year}-#{format('%02d', month)}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
[leading, weeks]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_rows(first, leading, weeks)
|
|
103
|
+
Array.new(weeks) do |row|
|
|
104
|
+
Array.new(7) do |column|
|
|
105
|
+
actual = first.add_days((row * 7) + column - leading)
|
|
106
|
+
in_month = actual.year == year && actual.month == month && actual.calendar == calendar
|
|
107
|
+
date = in_month || @include_adjacent ? actual : nil
|
|
108
|
+
Cell.new(date:, in_current_month: in_month, week_index: row, column_index: column)
|
|
109
|
+
end.freeze
|
|
110
|
+
end.freeze
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWCalendar
|
|
4
|
+
module ISO
|
|
5
|
+
# ISO week date value with Monday=1 through Sunday=7.
|
|
6
|
+
class WeekDate
|
|
7
|
+
WEEKDAY_NAMES = %w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday].freeze
|
|
8
|
+
attr_reader :week_year, :week, :weekday
|
|
9
|
+
|
|
10
|
+
# Create a valid ISO week-year, week, and weekday tuple.
|
|
11
|
+
def initialize(week_year, week, weekday)
|
|
12
|
+
unless week_year.is_a?(Integer) && week_year.positive?
|
|
13
|
+
raise InvalidISOWeekError, "ISO week-year must be a positive Integer"
|
|
14
|
+
end
|
|
15
|
+
raise InvalidISOWeekError, "ISO weekday must be in 1..7" unless weekday.is_a?(Integer) && weekday.between?(1, 7)
|
|
16
|
+
|
|
17
|
+
weeks = self.class.weeks_in_year(week_year)
|
|
18
|
+
unless week.is_a?(Integer) && week.between?(1, weeks)
|
|
19
|
+
raise InvalidISOWeekError, "ISO week must be in 1..#{weeks} for #{week_year}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@week_year = week_year
|
|
23
|
+
@week = week
|
|
24
|
+
@weekday = weekday
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Compute the ISO week date for an absolute civil date.
|
|
29
|
+
def self.for(date)
|
|
30
|
+
raise ArgumentError, "date must be a CivilDate" unless date.is_a?(CivilDate)
|
|
31
|
+
|
|
32
|
+
weekday = date.weekday
|
|
33
|
+
# The Thursday determines the ISO week-year, even for Julian labels.
|
|
34
|
+
thursday = CivilDate.from_jdn(date.to_jdn + 4 - weekday)
|
|
35
|
+
week_year = thursday.year
|
|
36
|
+
jan4 = Calendars::Gregorian.to_jdn(week_year, 1, 4)
|
|
37
|
+
week1_monday = jan4 - (jan4 % 7)
|
|
38
|
+
week = Arithmetic::FloorDivision.div(date.to_jdn - week1_monday, 7) + 1
|
|
39
|
+
new(week_year, week, weekday)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Return 52 or 53 according to the ISO week-year rule.
|
|
43
|
+
def self.weeks_in_year(year)
|
|
44
|
+
unless year.is_a?(Integer) && year.positive?
|
|
45
|
+
raise InvalidISOWeekError,
|
|
46
|
+
"ISO week-year must be a positive Integer"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
jan1_weekday = (Calendars::Gregorian.to_jdn(year, 1, 1) % 7) + 1
|
|
50
|
+
return 53 if jan1_weekday == 4 || (jan1_weekday == 3 && Calendars::Gregorian.leap_year?(year))
|
|
51
|
+
|
|
52
|
+
52
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Convert the ISO tuple to a proleptic Gregorian CivilDate.
|
|
56
|
+
def to_date
|
|
57
|
+
jan4 = Calendars::Gregorian.to_jdn(week_year, 1, 4)
|
|
58
|
+
week1_monday = jan4 - (jan4 % 7)
|
|
59
|
+
CivilDate.from_jdn(week1_monday + ((week - 1) * 7) + weekday - 1)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s
|
|
63
|
+
format("%<week_year>04d-W%<week>02d-%<weekday>d", week_year:, week:, weekday:)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def weekday_name
|
|
67
|
+
WEEKDAY_NAMES.fetch(weekday - 1)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ==(other)
|
|
71
|
+
other.is_a?(WeekDate) && [week_year, week, weekday] == [other.week_year, other.week, other.weekday]
|
|
72
|
+
end
|
|
73
|
+
alias eql? ==
|
|
74
|
+
|
|
75
|
+
def hash
|
|
76
|
+
[week_year, week, weekday].hash
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWCalendar
|
|
4
|
+
# Lazy inclusive range of dates from one calendar system.
|
|
5
|
+
class DateRange
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :start_date, :end_date
|
|
9
|
+
|
|
10
|
+
# Create an inclusive range with matching calendar systems.
|
|
11
|
+
def initialize(start_date, end_date)
|
|
12
|
+
unless start_date.is_a?(CivilDate) && end_date.is_a?(CivilDate)
|
|
13
|
+
raise ArgumentError, "range endpoints must be CivilDate objects"
|
|
14
|
+
end
|
|
15
|
+
unless start_date.calendar == end_date.calendar
|
|
16
|
+
raise InvalidCalendarError, "range endpoints must use the same calendar"
|
|
17
|
+
end
|
|
18
|
+
raise ArgumentError, "start_date must not follow end_date" if start_date > end_date
|
|
19
|
+
|
|
20
|
+
@start_date = start_date
|
|
21
|
+
@end_date = end_date
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Yield each date without first materializing the range.
|
|
26
|
+
def each
|
|
27
|
+
return enum_for(:each) unless block_given?
|
|
28
|
+
|
|
29
|
+
offset = 0
|
|
30
|
+
while offset <= count - 1
|
|
31
|
+
yield start_date.add_days(offset)
|
|
32
|
+
offset += 1
|
|
33
|
+
end
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Number of dates in the inclusive interval.
|
|
38
|
+
def count
|
|
39
|
+
start_date.days_until(end_date) + 1
|
|
40
|
+
end
|
|
41
|
+
alias size count
|
|
42
|
+
|
|
43
|
+
def include?(date)
|
|
44
|
+
date.is_a?(CivilDate) && date.calendar == start_date.calendar &&
|
|
45
|
+
date.to_jdn >= start_date.to_jdn && date.to_jdn <= end_date.to_jdn
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def first(number = nil)
|
|
49
|
+
return start_date if number.nil?
|
|
50
|
+
raise ArgumentError, "number must be a non-negative Integer" unless number.is_a?(Integer) && number >= 0
|
|
51
|
+
|
|
52
|
+
take(number)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def last(number = nil)
|
|
56
|
+
return end_date if number.nil?
|
|
57
|
+
raise ArgumentError, "number must be a non-negative Integer" unless number.is_a?(Integer) && number >= 0
|
|
58
|
+
return [] if number.zero?
|
|
59
|
+
|
|
60
|
+
first_index = [count - number, 0].max
|
|
61
|
+
(first_index...count).map { |offset| start_date.add_days(offset) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def step(days = 1)
|
|
65
|
+
raise ArgumentError, "step must be a positive Integer" unless days.is_a?(Integer) && days.positive?
|
|
66
|
+
return enum_for(:step, days) unless block_given?
|
|
67
|
+
|
|
68
|
+
offset = 0
|
|
69
|
+
while offset < count
|
|
70
|
+
yield start_date.add_days(offset)
|
|
71
|
+
offset += days
|
|
72
|
+
end
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def weekdays
|
|
77
|
+
return enum_for(:weekdays) unless block_given?
|
|
78
|
+
|
|
79
|
+
each { |date| yield date unless [6, 7].include?(date.weekday) }
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/jw_calendar.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jw_calendar/version"
|
|
4
|
+
require "jw_calendar/errors"
|
|
5
|
+
require "jw_calendar/arithmetic/floor_division"
|
|
6
|
+
require "jw_calendar/calendars/gregorian"
|
|
7
|
+
require "jw_calendar/calendars/julian"
|
|
8
|
+
require "jw_calendar/civil_date"
|
|
9
|
+
require "jw_calendar/conversion/julian_day_number"
|
|
10
|
+
require "jw_calendar/conversion/calendar_converter"
|
|
11
|
+
require "jw_calendar/conversion/ordinal_date"
|
|
12
|
+
require "jw_calendar/iso/week_date"
|
|
13
|
+
require "jw_calendar/calendars/reform_calendar"
|
|
14
|
+
require "jw_calendar/grid/cell"
|
|
15
|
+
require "jw_calendar/grid/month_grid"
|
|
16
|
+
require "jw_calendar/range/date_range"
|
|
17
|
+
require "jw_calendar/boundary/report"
|
|
18
|
+
require "jw_calendar/boundary/analyzer"
|
|
19
|
+
require "jw_calendar/formatting/iso8601"
|
|
20
|
+
|
|
21
|
+
# Deterministic civil-date arithmetic for Gregorian, Julian, and reform calendars.
|
|
22
|
+
module JWCalendar
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jw_calendar
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- JW Calendar
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
A dependency-free civil-calendar engine for Ruby with proleptic Gregorian and Julian arithmetic,
|
|
14
|
+
Julian Day Number conversions, ISO week and ordinal dates, month grids, reform cutovers, and
|
|
15
|
+
boundary analysis. Civil dates are kept separate from instants and time zones.
|
|
16
|
+
email: []
|
|
17
|
+
executables:
|
|
18
|
+
- jwcalendar
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- CODE_OF_CONDUCT.md
|
|
24
|
+
- CONTRIBUTING.md
|
|
25
|
+
- LICENSE.txt
|
|
26
|
+
- README.md
|
|
27
|
+
- SECURITY.md
|
|
28
|
+
- docs/architecture.md
|
|
29
|
+
- docs/civil-dates.md
|
|
30
|
+
- docs/cli.md
|
|
31
|
+
- docs/correctness.md
|
|
32
|
+
- docs/gregorian.md
|
|
33
|
+
- docs/iso-week.md
|
|
34
|
+
- docs/julian-day-number.md
|
|
35
|
+
- docs/julian.md
|
|
36
|
+
- docs/month-grid.md
|
|
37
|
+
- docs/reform-calendars.md
|
|
38
|
+
- exe/jwcalendar
|
|
39
|
+
- lib/jw_calendar.rb
|
|
40
|
+
- lib/jw_calendar/arithmetic/floor_division.rb
|
|
41
|
+
- lib/jw_calendar/boundary/analyzer.rb
|
|
42
|
+
- lib/jw_calendar/boundary/report.rb
|
|
43
|
+
- lib/jw_calendar/calendars/gregorian.rb
|
|
44
|
+
- lib/jw_calendar/calendars/julian.rb
|
|
45
|
+
- lib/jw_calendar/calendars/reform_calendar.rb
|
|
46
|
+
- lib/jw_calendar/civil_date.rb
|
|
47
|
+
- lib/jw_calendar/cli/runner.rb
|
|
48
|
+
- lib/jw_calendar/conversion/calendar_converter.rb
|
|
49
|
+
- lib/jw_calendar/conversion/julian_day_number.rb
|
|
50
|
+
- lib/jw_calendar/conversion/ordinal_date.rb
|
|
51
|
+
- lib/jw_calendar/errors.rb
|
|
52
|
+
- lib/jw_calendar/formatting/iso8601.rb
|
|
53
|
+
- lib/jw_calendar/grid/cell.rb
|
|
54
|
+
- lib/jw_calendar/grid/month_grid.rb
|
|
55
|
+
- lib/jw_calendar/iso/week_date.rb
|
|
56
|
+
- lib/jw_calendar/range/date_range.rb
|
|
57
|
+
- lib/jw_calendar/version.rb
|
|
58
|
+
homepage: https://jwcalendar.com/
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
homepage_uri: https://jwcalendar.com/
|
|
63
|
+
source_code_uri: https://github.com/karencohenjw/jw_calendar
|
|
64
|
+
documentation_uri: https://github.com/karencohenjw/jw_calendar/tree/main/docs
|
|
65
|
+
changelog_uri: https://github.com/karencohenjw/jw_calendar/blob/main/CHANGELOG.md
|
|
66
|
+
bug_tracker_uri: https://github.com/karencohenjw/jw_calendar/issues
|
|
67
|
+
rubygems_mfa_required: 'true'
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.3'
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 4.0.20
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: Deterministic Gregorian, Julian, ISO week-date and calendar-grid utilities
|
|
85
|
+
for Ruby.
|
|
86
|
+
test_files: []
|