beta_calendars 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 +9 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/CONTRIBUTING.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +222 -0
- data/Rakefile +11 -0
- data/beta_calendars.gemspec +59 -0
- data/exe/beta-calendars +6 -0
- data/lib/beta_calendars/blank_grid.rb +56 -0
- data/lib/beta_calendars/cli.rb +147 -0
- data/lib/beta_calendars/day_cell.rb +40 -0
- data/lib/beta_calendars/errors.rb +14 -0
- data/lib/beta_calendars/iso_week.rb +21 -0
- data/lib/beta_calendars/month.rb +77 -0
- data/lib/beta_calendars/serializable.rb +9 -0
- data/lib/beta_calendars/version.rb +5 -0
- data/lib/beta_calendars/year.rb +47 -0
- data/lib/beta_calendars.rb +136 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 49e4c5c571e4c9878ab8006fe05159c07a3e3dc86c48a73d425b40368e3d74c2
|
|
4
|
+
data.tar.gz: a4ba1e530f4f2ed9ee885fdf3cd9b53fbb388628b9ed6362c3d90822b195ed12
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8c96ba768ca43572525d54f7abcb5d3b826fec126b52389070b6370769dfc8d444c2f1a15ae3f6995c88afbb4b22ad397fe6db66eccb99e24c8bdff436aec103
|
|
7
|
+
data.tar.gz: eabe8fdaaa4628ce79a5169f96a114bd872a9e56e0ece0a56dd04b2f6f0680e2a9116399a42a5d9a6238a83c500df99fafc75bec0d12a86717cf168e45c30918
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Generate month and year calendar grids with Sunday or Monday week starts.
|
|
6
|
+
- Include optional adjacent-month dates and six-row print layouts.
|
|
7
|
+
- Provide blank planning grids and ISO week metadata.
|
|
8
|
+
- Serialize calendar structures to hashes and JSON.
|
|
9
|
+
- Add the `beta-calendars` command-line interface.
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our pledge
|
|
4
|
+
|
|
5
|
+
We are committed to making participation in this project a respectful,
|
|
6
|
+
welcoming experience for everyone, regardless of background or identity.
|
|
7
|
+
|
|
8
|
+
## Our standards
|
|
9
|
+
|
|
10
|
+
Examples of behavior that contributes to a positive environment include being
|
|
11
|
+
kind, considering differing viewpoints, giving and accepting constructive
|
|
12
|
+
feedback, and taking responsibility for mistakes.
|
|
13
|
+
|
|
14
|
+
Unacceptable behavior includes harassment, insults, personal attacks, and
|
|
15
|
+
publishing another person's private information without permission.
|
|
16
|
+
|
|
17
|
+
## Enforcement
|
|
18
|
+
|
|
19
|
+
Report conduct concerns through the project's GitHub issue tracker. Project
|
|
20
|
+
maintainers will review reports and take appropriate, proportionate action.
|
|
21
|
+
|
|
22
|
+
This code of conduct is adapted from the Contributor Covenant, version 2.1.
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Bug reports, focused feature proposals, and pull requests are welcome. Please
|
|
4
|
+
include a small example that shows the expected calendar dates and week-start
|
|
5
|
+
policy when reporting a calculation issue.
|
|
6
|
+
|
|
7
|
+
## Development
|
|
8
|
+
|
|
9
|
+
The library targets Ruby 3.1 and newer and has no runtime gem dependencies.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle install
|
|
13
|
+
bundle exec rake test
|
|
14
|
+
gem build beta_calendars.gemspec
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Keep changes deterministic, use Ruby's proleptic Gregorian `Date` calculations,
|
|
18
|
+
and add meaningful tests for boundary dates. Do not add network access to the
|
|
19
|
+
calendar computation path.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mateo Pedersen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Beta Calendars Ruby
|
|
2
|
+
|
|
3
|
+
**Deterministic Gregorian calendar grids for Ruby applications.** Generate monthly and yearly calendar data locally, with week-start policies, ISO week metadata, adjacent dates, fixed six-week layouts, blank planning grids, JSON output, and a small CLI.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
A calendar grid looks simple, but correct output depends on leap years, weekday offsets, Sunday or Monday week starts, ISO week-year boundaries, and the geometry chosen for printing. This library turns those rules into a predictable data model without a web service, scraping, or runtime gem dependencies.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
gem install beta_calendars
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add it to a Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "beta_calendars"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The gem requires Ruby 3.1 or newer and uses Ruby's standard `Date` and `JSON` libraries.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "beta_calendars"
|
|
27
|
+
|
|
28
|
+
calendar = BetaCalendars.month(year: 2027, month: 1, week_start: :monday)
|
|
29
|
+
|
|
30
|
+
calendar.year # => 2027
|
|
31
|
+
calendar.month # => 1
|
|
32
|
+
calendar.month_name # => "January"
|
|
33
|
+
calendar.days.size # => 31
|
|
34
|
+
calendar.weeks # => rows of seven DayCell values
|
|
35
|
+
calendar.to_h # => developer-friendly nested hash
|
|
36
|
+
calendar.to_json # => JSON with ISO-8601 date strings
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`DayCell` instances are immutable and expose `date`, `day`, `weekday`,
|
|
40
|
+
`iso_week`, `iso_week_year`, `in_current_month`, and `weekend`. `weekday` follows
|
|
41
|
+
Ruby's convention, where Sunday is `0`; `iso_weekday` follows ISO-8601, where
|
|
42
|
+
Monday is `1` and Sunday is `7`.
|
|
43
|
+
|
|
44
|
+
## Monthly Calendars
|
|
45
|
+
|
|
46
|
+
`BetaCalendars.month` returns a month object. Its `days` array contains only
|
|
47
|
+
dates in the requested month, while `weeks` and `matrix` contain the display
|
|
48
|
+
grid. Each populated position is a `DayCell`, not just a day number.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
january = BetaCalendars.month(year: 2027, month: 1)
|
|
52
|
+
january.month_name # => "January"
|
|
53
|
+
january.days.first.date.iso8601 # => "2027-01-01"
|
|
54
|
+
january.weeks.first.first.to_h
|
|
55
|
+
# => {:date=>"2026-12-27", :day=>27, ...}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The grid uses the natural four, five, or six rows required by that month unless
|
|
59
|
+
`fixed_six_weeks: true` is selected.
|
|
60
|
+
|
|
61
|
+
## Monday vs Sunday Week Start
|
|
62
|
+
|
|
63
|
+
Choose the first weekday explicitly. The API accepts `:monday` and `:sunday`
|
|
64
|
+
(or their string forms); invalid values raise `BetaCalendars::InvalidWeekStartError`.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
sunday = BetaCalendars.month(year: 2027, month: 1, week_start: :sunday)
|
|
68
|
+
monday = BetaCalendars.month(year: 2027, month: 1, week_start: :monday)
|
|
69
|
+
|
|
70
|
+
sunday.weekdays # => ["Sunday", "Monday", ...]
|
|
71
|
+
monday.weekdays # => ["Monday", "Tuesday", ...]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Adjacent Month Dates
|
|
75
|
+
|
|
76
|
+
Out-of-month cells contain the neighboring real dates by default. Set
|
|
77
|
+
`adjacent_dates: false` to leave those cells as `nil`; this is useful when a
|
|
78
|
+
renderer wants visibly empty leading and trailing cells.
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
filled = BetaCalendars.month(year: 2027, month: 1, adjacent_dates: true)
|
|
82
|
+
empty = BetaCalendars.month(year: 2027, month: 1, adjacent_dates: false)
|
|
83
|
+
|
|
84
|
+
filled.weeks.first.first.date.iso8601 # => "2026-12-27"
|
|
85
|
+
empty.weeks.first.first # => nil
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Fixed Six-Week Print Layouts
|
|
89
|
+
|
|
90
|
+
For printable month pages that need identical geometry, set
|
|
91
|
+
`fixed_six_weeks: true`. Every month then has exactly six rows and seven
|
|
92
|
+
columns, including months that naturally need only four or five rows.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
print_month = BetaCalendars.month(
|
|
96
|
+
year: 2027,
|
|
97
|
+
month: 2,
|
|
98
|
+
week_start: :monday,
|
|
99
|
+
fixed_six_weeks: true
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
print_month.weeks.length # => 6
|
|
103
|
+
print_month.weeks.all? { |week| week.length == 7 } # => true
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
With `adjacent_dates: false`, positions outside February remain `nil` even in
|
|
107
|
+
the sixth row. The library supplies geometry; the application decides whether
|
|
108
|
+
those positions render as whitespace, lines, or another design element.
|
|
109
|
+
|
|
110
|
+
## Year Calendars
|
|
111
|
+
|
|
112
|
+
`BetaCalendars.year` contains all twelve month objects. Indexing is one-based,
|
|
113
|
+
so `year[1]` is January and `year[12]` is December.
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
calendar_year = BetaCalendars.year(year: 2027, week_start: :monday)
|
|
117
|
+
calendar_year.months.length # => 12
|
|
118
|
+
calendar_year[1].month_name # => "January"
|
|
119
|
+
calendar_year[12].month_name # => "December"
|
|
120
|
+
calendar_year.to_h
|
|
121
|
+
calendar_year.to_json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Blank Calendar Grids
|
|
125
|
+
|
|
126
|
+
`blank_grid` creates an empty seven-column planning structure with weekday
|
|
127
|
+
labels and row/column metadata. It does not assign dates to the cells.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
planning_grid = BetaCalendars.blank_grid(rows: 6, week_start: :sunday)
|
|
131
|
+
planning_grid.weeks.length # => 6
|
|
132
|
+
planning_grid.weeks.first.length # => 7
|
|
133
|
+
planning_grid.weeks.first.first.to_h
|
|
134
|
+
# => {:row=>0, :column=>0, :weekday=>"Sunday"}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For a printable blank calendar example, see the [Beta Calendars blank
|
|
138
|
+
calendar](https://www.betacalendars.com/blank-calendar). The returned grid is
|
|
139
|
+
generic and can be used in unrelated planning applications.
|
|
140
|
+
|
|
141
|
+
## JSON Output
|
|
142
|
+
|
|
143
|
+
All calendar value objects implement `to_h` and `to_json`. Dates serialize as
|
|
144
|
+
ISO-8601 strings such as `"2027-01-01"`; ISO week and week-year are separate
|
|
145
|
+
integer fields, so year-boundary meaning is preserved.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
require "json"
|
|
149
|
+
|
|
150
|
+
calendar = BetaCalendars.month(year: 2027, month: 1, week_start: :monday)
|
|
151
|
+
json = calendar.to_json
|
|
152
|
+
parsed = JSON.parse(json)
|
|
153
|
+
parsed.fetch("weeks").first
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
You can also get the plain two-dimensional cell array directly:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
matrix = BetaCalendars.month_matrix(
|
|
160
|
+
year: 2027,
|
|
161
|
+
month: 1,
|
|
162
|
+
week_start: :monday,
|
|
163
|
+
adjacent_dates: false
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## CLI
|
|
168
|
+
|
|
169
|
+
The installed `beta-calendars` executable offers readable terminal output or
|
|
170
|
+
JSON. Options may follow the command arguments.
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
beta-calendars month 2027 1
|
|
174
|
+
beta-calendars month 2027 1 --week-start monday
|
|
175
|
+
beta-calendars month 2027 1 --json
|
|
176
|
+
beta-calendars month 2027 2 --fixed-six-weeks --no-adjacent-dates
|
|
177
|
+
beta-calendars year 2027 --json
|
|
178
|
+
beta-calendars blank --rows 6 --week-start sunday
|
|
179
|
+
beta-calendars info 2027
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Calendar Correctness
|
|
183
|
+
|
|
184
|
+
The implementation uses Ruby's `Date` with the proleptic Gregorian calendar
|
|
185
|
+
for month length, weekday placement, and ISO-8601 week calculations. It treats
|
|
186
|
+
calendar dates as date-only values: there are no time zones, daylight-saving
|
|
187
|
+
transitions, or remote data sources in the calculation path. Supported years
|
|
188
|
+
are 1 through 9999.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
BetaCalendars.leap_year?(2024) # => true
|
|
192
|
+
BetaCalendars.days_in_month(2027, 2) # => 28
|
|
193
|
+
BetaCalendars.iso_week(Date.new(2021, 1, 1)).to_h # => {:week=>53, :year=>2020}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Printing and Presentation
|
|
197
|
+
|
|
198
|
+
This library produces calendar data models. Your application chooses how to
|
|
199
|
+
render them as HTML, PDF, images, terminal output, or another format. For
|
|
200
|
+
printable calendar examples and planning resources, see
|
|
201
|
+
[Beta Calendars](https://www.betacalendars.com/).
|
|
202
|
+
|
|
203
|
+
## Related resources
|
|
204
|
+
|
|
205
|
+
- [Monthly calendar examples](https://www.betacalendars.com/monthly-calendar) show the month-grid use case supported by `BetaCalendars.month`.
|
|
206
|
+
- [Monthly planner pages](https://www.betacalendars.com/monthly-planner) are a related planning use case for the library's blank grids.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
bundle install
|
|
212
|
+
bundle exec rake test
|
|
213
|
+
gem build beta_calendars.gemspec
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The test suite covers leap years, month geometry, both week starts, adjacent
|
|
217
|
+
dates, ISO week-year boundaries, fixed print grids, blank grids, JSON output,
|
|
218
|
+
and the executable.
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/beta_calendars/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "beta_calendars"
|
|
7
|
+
spec.version = BetaCalendars::VERSION
|
|
8
|
+
spec.authors = ["Mateo Pedersen"]
|
|
9
|
+
spec.email = []
|
|
10
|
+
|
|
11
|
+
spec.summary = "Deterministic Ruby calendar grids for monthly, yearly and printable calendar applications."
|
|
12
|
+
spec.description = <<~DESCRIPTION
|
|
13
|
+
Beta Calendars Ruby generates Gregorian month and year calendar structures,
|
|
14
|
+
including configurable Sunday or Monday week starts, adjacent-month cells,
|
|
15
|
+
ISO week metadata, fixed six-week print grids, blank planning grids, JSON
|
|
16
|
+
serialization, and a small command-line interface. All calculations run
|
|
17
|
+
locally using Ruby's standard Date library.
|
|
18
|
+
DESCRIPTION
|
|
19
|
+
spec.homepage = "https://www.betacalendars.com/"
|
|
20
|
+
spec.license = "MIT"
|
|
21
|
+
spec.required_ruby_version = ">= 3.1"
|
|
22
|
+
|
|
23
|
+
spec.metadata = {
|
|
24
|
+
"homepage_uri" => spec.homepage,
|
|
25
|
+
"source_code_uri" => "https://github.com/mateopedersen/beta-calendars-ruby",
|
|
26
|
+
"documentation_uri" => "https://github.com/mateopedersen/beta-calendars-ruby#readme",
|
|
27
|
+
"changelog_uri" => "https://github.com/mateopedersen/beta-calendars-ruby/blob/main/CHANGELOG.md",
|
|
28
|
+
"bug_tracker_uri" => "https://github.com/mateopedersen/beta-calendars-ruby/issues",
|
|
29
|
+
"rubygems_mfa_required" => "true"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# An explicit allowlist keeps local credentials, build products, and unrelated
|
|
33
|
+
# project files out of the published gem.
|
|
34
|
+
spec.files = %w[
|
|
35
|
+
CHANGELOG.md
|
|
36
|
+
CODE_OF_CONDUCT.md
|
|
37
|
+
CONTRIBUTING.md
|
|
38
|
+
LICENSE.txt
|
|
39
|
+
README.md
|
|
40
|
+
Rakefile
|
|
41
|
+
beta_calendars.gemspec
|
|
42
|
+
exe/beta-calendars
|
|
43
|
+
lib/beta_calendars.rb
|
|
44
|
+
lib/beta_calendars/blank_grid.rb
|
|
45
|
+
lib/beta_calendars/cli.rb
|
|
46
|
+
lib/beta_calendars/day_cell.rb
|
|
47
|
+
lib/beta_calendars/errors.rb
|
|
48
|
+
lib/beta_calendars/iso_week.rb
|
|
49
|
+
lib/beta_calendars/month.rb
|
|
50
|
+
lib/beta_calendars/serializable.rb
|
|
51
|
+
lib/beta_calendars/version.rb
|
|
52
|
+
lib/beta_calendars/year.rb
|
|
53
|
+
]
|
|
54
|
+
spec.bindir = "exe"
|
|
55
|
+
spec.executables = ["beta-calendars"]
|
|
56
|
+
spec.require_paths = ["lib"]
|
|
57
|
+
spec.add_development_dependency "minitest", ">= 5.0", "< 6"
|
|
58
|
+
spec.add_development_dependency "rake", ">= 12.3", "< 14"
|
|
59
|
+
end
|
data/exe/beta-calendars
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetaCalendars
|
|
4
|
+
# A position in a date-free planning grid.
|
|
5
|
+
class BlankCell
|
|
6
|
+
include Serializable
|
|
7
|
+
|
|
8
|
+
attr_reader :row, :column, :weekday
|
|
9
|
+
|
|
10
|
+
def initialize(row:, column:, weekday:)
|
|
11
|
+
@row = row
|
|
12
|
+
@column = column
|
|
13
|
+
@weekday = weekday
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{ row: row, column: column, weekday: weekday }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A blank rows-by-seven structure ready for a planner or printable renderer.
|
|
23
|
+
class BlankGrid
|
|
24
|
+
include Serializable
|
|
25
|
+
|
|
26
|
+
attr_reader :rows, :week_start, :weekdays, :weeks
|
|
27
|
+
|
|
28
|
+
def initialize(rows:, week_start:)
|
|
29
|
+
unless rows.is_a?(Integer) && rows.between?(1, 60)
|
|
30
|
+
raise InvalidGridSizeError, "rows must be an integer from 1 to 60"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@rows = rows
|
|
34
|
+
@week_start = CalendarValidation.normalize_week_start(week_start)
|
|
35
|
+
@weekdays = Month::WEEKDAY_NAMES.rotate(week_start == :sunday ? 0 : 1).freeze
|
|
36
|
+
@weeks = rows.times.map do |row|
|
|
37
|
+
7.times.map do |column|
|
|
38
|
+
BlankCell.new(row: row, column: column, weekday: weekdays[column])
|
|
39
|
+
end.freeze
|
|
40
|
+
end.freeze
|
|
41
|
+
freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias grid weeks
|
|
45
|
+
|
|
46
|
+
def to_h
|
|
47
|
+
{
|
|
48
|
+
rows: rows,
|
|
49
|
+
columns: 7,
|
|
50
|
+
week_start: week_start,
|
|
51
|
+
weekdays: weekdays,
|
|
52
|
+
weeks: weeks.map { |week| week.map(&:to_h).freeze }.freeze
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module BetaCalendars
|
|
6
|
+
# Small standard-library command-line interface.
|
|
7
|
+
module CLI
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def run(arguments, out: $stdout, err: $stderr)
|
|
11
|
+
args = arguments.dup
|
|
12
|
+
command = args.shift
|
|
13
|
+
return help(out) if command.nil? || %w[help --help -h].include?(command)
|
|
14
|
+
|
|
15
|
+
options = { week_start: :sunday, adjacent_dates: true, fixed_six_weeks: false, json: false, rows: 6 }
|
|
16
|
+
parser = option_parser(options)
|
|
17
|
+
parser.parse!(args)
|
|
18
|
+
|
|
19
|
+
case command
|
|
20
|
+
when "month"
|
|
21
|
+
run_month(args, options, out)
|
|
22
|
+
when "year"
|
|
23
|
+
run_year(args, options, out)
|
|
24
|
+
when "blank"
|
|
25
|
+
run_blank(args, options, out)
|
|
26
|
+
when "info"
|
|
27
|
+
run_info(args, options, out)
|
|
28
|
+
else
|
|
29
|
+
raise OptionParser::InvalidArgument, "unknown command #{command.inspect}"
|
|
30
|
+
end
|
|
31
|
+
0
|
|
32
|
+
rescue OptionParser::ParseError, ArgumentError, BetaCalendars::Error => error
|
|
33
|
+
err.puts("beta-calendars: #{error.message}")
|
|
34
|
+
2
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def option_parser(options)
|
|
38
|
+
OptionParser.new do |parser|
|
|
39
|
+
parser.banner = "Usage: beta-calendars COMMAND [arguments] [options]"
|
|
40
|
+
parser.on("--week-start START", "Week start: monday or sunday") { |value| options[:week_start] = value.to_sym }
|
|
41
|
+
parser.on("--fixed-six-weeks", "Force monthly grids to six rows") { options[:fixed_six_weeks] = true }
|
|
42
|
+
parser.on("--no-adjacent-dates", "Leave out-of-month cells empty") { options[:adjacent_dates] = false }
|
|
43
|
+
parser.on("--rows COUNT", Integer, "Blank grid rows (1-60)") { |value| options[:rows] = value }
|
|
44
|
+
parser.on("--json", "Print JSON") { options[:json] = true }
|
|
45
|
+
parser.on("-h", "--help", "Show this help") { raise OptionParser::InvalidArgument, parser.to_s }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run_month(args, options, out)
|
|
50
|
+
require_count!(args, 2, "month YEAR MONTH")
|
|
51
|
+
calendar = BetaCalendars.month(
|
|
52
|
+
year: integer!(args[0], "year"),
|
|
53
|
+
month: integer!(args[1], "month"),
|
|
54
|
+
week_start: options[:week_start],
|
|
55
|
+
adjacent_dates: options[:adjacent_dates],
|
|
56
|
+
fixed_six_weeks: options[:fixed_six_weeks]
|
|
57
|
+
)
|
|
58
|
+
options[:json] ? out.puts(calendar.to_json) : print_month(calendar, out)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run_year(args, options, out)
|
|
62
|
+
require_count!(args, 1, "year YEAR")
|
|
63
|
+
calendar = BetaCalendars.year(
|
|
64
|
+
year: integer!(args[0], "year"),
|
|
65
|
+
week_start: options[:week_start],
|
|
66
|
+
adjacent_dates: options[:adjacent_dates],
|
|
67
|
+
fixed_six_weeks: options[:fixed_six_weeks]
|
|
68
|
+
)
|
|
69
|
+
if options[:json]
|
|
70
|
+
out.puts(calendar.to_json)
|
|
71
|
+
else
|
|
72
|
+
out.puts("#{calendar.year} Calendar (weeks start #{calendar.week_start})")
|
|
73
|
+
calendar.months.each { |month| print_month(month, out) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def run_blank(args, options, out)
|
|
78
|
+
require_count!(args, 0, "blank")
|
|
79
|
+
grid = BetaCalendars.blank_grid(rows: options[:rows], week_start: options[:week_start])
|
|
80
|
+
if options[:json]
|
|
81
|
+
out.puts(grid.to_json)
|
|
82
|
+
else
|
|
83
|
+
out.puts(grid.weekdays.map { |name| name[0, 2] }.join(" "))
|
|
84
|
+
grid.weeks.each { out.puts(Array.new(7, " ").join(" ")) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def run_info(args, options, out)
|
|
89
|
+
require_count!(args, 1, "info YEAR")
|
|
90
|
+
year = integer!(args[0], "year")
|
|
91
|
+
info = {
|
|
92
|
+
year: year,
|
|
93
|
+
leap_year: BetaCalendars.leap_year?(year),
|
|
94
|
+
months: (1..12).map do |month_number|
|
|
95
|
+
{ month: month_number, name: Date::MONTHNAMES[month_number], days: BetaCalendars.days_in_month(year, month_number) }
|
|
96
|
+
end
|
|
97
|
+
}
|
|
98
|
+
options[:json] ? out.puts(JSON.generate(info)) : print_info(info, out)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def print_month(calendar, out)
|
|
102
|
+
out.puts("#{calendar.month_name} #{calendar.year}")
|
|
103
|
+
out.puts(calendar.weekdays.map { |name| name[0, 2] }.join(" "))
|
|
104
|
+
calendar.weeks.each do |week|
|
|
105
|
+
out.puts(week.map { |cell| cell ? format("%2d", cell.day) : " " }.join(" "))
|
|
106
|
+
end
|
|
107
|
+
out.puts
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def print_info(info, out)
|
|
111
|
+
out.puts("#{info[:year]}: #{info[:leap_year] ? 'leap year' : 'common year'}")
|
|
112
|
+
info[:months].each { |month| out.puts(format("%-9s %2d days", month[:name], month[:days])) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def integer!(value, label)
|
|
116
|
+
Integer(value, 10)
|
|
117
|
+
rescue ArgumentError
|
|
118
|
+
raise OptionParser::InvalidArgument, "#{label} must be an integer"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def require_count!(args, expected, usage)
|
|
122
|
+
return if args.length == expected
|
|
123
|
+
|
|
124
|
+
raise OptionParser::InvalidArgument, "expected #{usage}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def help(out)
|
|
128
|
+
out.puts <<~HELP
|
|
129
|
+
Usage: beta-calendars COMMAND [arguments] [options]
|
|
130
|
+
|
|
131
|
+
Commands:
|
|
132
|
+
month YEAR MONTH Print one month grid
|
|
133
|
+
year YEAR Print all twelve months
|
|
134
|
+
blank Print a blank planning grid
|
|
135
|
+
info YEAR Show leap-year and month-length information
|
|
136
|
+
|
|
137
|
+
Options:
|
|
138
|
+
--week-start monday|sunday
|
|
139
|
+
--fixed-six-weeks
|
|
140
|
+
--no-adjacent-dates
|
|
141
|
+
--rows COUNT Blank grid rows (1-60)
|
|
142
|
+
--json
|
|
143
|
+
HELP
|
|
144
|
+
0
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetaCalendars
|
|
4
|
+
# Immutable metadata for one real Gregorian date in a month grid.
|
|
5
|
+
class DayCell
|
|
6
|
+
include Serializable
|
|
7
|
+
|
|
8
|
+
attr_reader :date, :day, :year, :month, :weekday, :iso_weekday,
|
|
9
|
+
:iso_week, :iso_week_year, :in_current_month, :weekend
|
|
10
|
+
|
|
11
|
+
def initialize(date:, current_year:, current_month:)
|
|
12
|
+
@date = date
|
|
13
|
+
@day = date.day
|
|
14
|
+
@year = date.year
|
|
15
|
+
@month = date.month
|
|
16
|
+
@weekday = date.wday # Sunday = 0, matching Ruby Date#wday.
|
|
17
|
+
@iso_weekday = date.cwday # Monday = 1 through Sunday = 7.
|
|
18
|
+
@iso_week = date.cweek
|
|
19
|
+
@iso_week_year = date.cwyear
|
|
20
|
+
@in_current_month = (date.year == current_year && date.month == current_month)
|
|
21
|
+
@weekend = (date.wday == 0 || date.wday == 6)
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
date: date.iso8601,
|
|
28
|
+
day: day,
|
|
29
|
+
year: year,
|
|
30
|
+
month: month,
|
|
31
|
+
weekday: weekday,
|
|
32
|
+
iso_weekday: iso_weekday,
|
|
33
|
+
iso_week: iso_week,
|
|
34
|
+
iso_week_year: iso_week_year,
|
|
35
|
+
in_current_month: in_current_month,
|
|
36
|
+
weekend: weekend
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetaCalendars
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class InvalidWeekStartError < ArgumentError
|
|
7
|
+
def initialize(value)
|
|
8
|
+
super("week_start must be :monday or :sunday (got #{value.inspect})")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class InvalidCalendarDateError < ArgumentError; end
|
|
13
|
+
class InvalidGridSizeError < ArgumentError; end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetaCalendars
|
|
4
|
+
# ISO-8601 week number and the week-based year that owns it.
|
|
5
|
+
class ISOWeek
|
|
6
|
+
include Serializable
|
|
7
|
+
|
|
8
|
+
attr_reader :week, :year
|
|
9
|
+
alias iso_year year
|
|
10
|
+
|
|
11
|
+
def initialize(week:, year:)
|
|
12
|
+
@week = week
|
|
13
|
+
@year = year
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{ week: week, year: year }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetaCalendars
|
|
4
|
+
# Calendar grid and date metadata for one Gregorian month.
|
|
5
|
+
class Month
|
|
6
|
+
include Serializable
|
|
7
|
+
|
|
8
|
+
WEEKDAY_NAMES = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :year, :month, :week_start, :adjacent_dates, :fixed_six_weeks,
|
|
11
|
+
:days_in_month, :weeks, :weekdays
|
|
12
|
+
|
|
13
|
+
def initialize(year:, month:, week_start:, adjacent_dates:, fixed_six_weeks:)
|
|
14
|
+
CalendarValidation.validate_year!(year)
|
|
15
|
+
CalendarValidation.validate_month!(month)
|
|
16
|
+
@year = year
|
|
17
|
+
@month = month
|
|
18
|
+
@week_start = CalendarValidation.normalize_week_start(week_start)
|
|
19
|
+
@adjacent_dates = !!adjacent_dates
|
|
20
|
+
@fixed_six_weeks = !!fixed_six_weeks
|
|
21
|
+
@days_in_month = Date.new(year, month, -1, Date::GREGORIAN).day
|
|
22
|
+
@weekdays = ordered_weekday_names.freeze
|
|
23
|
+
@weeks = build_weeks.freeze
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def month_name
|
|
28
|
+
Date::MONTHNAMES.fetch(month)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns one immutable cell for each day in this month, excluding adjacent dates.
|
|
32
|
+
def days
|
|
33
|
+
(1..days_in_month).map do |day|
|
|
34
|
+
DayCell.new(date: Date.new(year, month, day, Date::GREGORIAN), current_year: year, current_month: month)
|
|
35
|
+
end.freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alias matrix weeks
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
year: year,
|
|
43
|
+
month: month,
|
|
44
|
+
month_name: month_name,
|
|
45
|
+
week_start: week_start,
|
|
46
|
+
adjacent_dates: adjacent_dates,
|
|
47
|
+
fixed_six_weeks: fixed_six_weeks,
|
|
48
|
+
weekdays: weekdays,
|
|
49
|
+
weeks: weeks.map { |week| week.map { |cell| cell&.to_h }.freeze }.freeze
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def ordered_weekday_names
|
|
56
|
+
start = (week_start == :sunday ? 0 : 1)
|
|
57
|
+
7.times.map { |offset| WEEKDAY_NAMES[(start + offset) % 7] }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_weeks
|
|
61
|
+
first_date = Date.new(year, month, 1, Date::GREGORIAN)
|
|
62
|
+
start_weekday = week_start == :sunday ? 0 : 1
|
|
63
|
+
leading = (first_date.wday - start_weekday) % 7
|
|
64
|
+
row_count = fixed_six_weeks ? 6 : ((leading + days_in_month + 6) / 7)
|
|
65
|
+
grid_start = first_date - leading
|
|
66
|
+
|
|
67
|
+
row_count.times.map do |row|
|
|
68
|
+
7.times.map do |column|
|
|
69
|
+
date = grid_start + (row * 7) + column
|
|
70
|
+
if adjacent_dates || (date.year == year && date.month == month)
|
|
71
|
+
DayCell.new(date: date, current_year: year, current_month: month)
|
|
72
|
+
end
|
|
73
|
+
end.freeze
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetaCalendars
|
|
4
|
+
# Twelve month value objects for one Gregorian calendar year.
|
|
5
|
+
class Year
|
|
6
|
+
include Serializable
|
|
7
|
+
|
|
8
|
+
attr_reader :year, :week_start, :adjacent_dates, :fixed_six_weeks, :months
|
|
9
|
+
|
|
10
|
+
def initialize(year:, week_start:, adjacent_dates:, fixed_six_weeks:)
|
|
11
|
+
CalendarValidation.validate_year!(year)
|
|
12
|
+
@year = year
|
|
13
|
+
@week_start = CalendarValidation.normalize_week_start(week_start)
|
|
14
|
+
@adjacent_dates = !!adjacent_dates
|
|
15
|
+
@fixed_six_weeks = !!fixed_six_weeks
|
|
16
|
+
@months = (1..12).map do |month_number|
|
|
17
|
+
Month.new(
|
|
18
|
+
year: year,
|
|
19
|
+
month: month_number,
|
|
20
|
+
week_start: @week_start,
|
|
21
|
+
adjacent_dates: @adjacent_dates,
|
|
22
|
+
fixed_six_weeks: @fixed_six_weeks
|
|
23
|
+
)
|
|
24
|
+
end.freeze
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Month numbers are one-based: year[1] is January and year[12] is December.
|
|
29
|
+
def [](month_number)
|
|
30
|
+
unless month_number.is_a?(Integer) && month_number.between?(1, 12)
|
|
31
|
+
raise IndexError, "month number must be between 1 and 12"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
months.fetch(month_number - 1)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
{
|
|
39
|
+
year: year,
|
|
40
|
+
week_start: week_start,
|
|
41
|
+
adjacent_dates: adjacent_dates,
|
|
42
|
+
fixed_six_weeks: fixed_six_weeks,
|
|
43
|
+
months: months.map(&:to_h).freeze
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "beta_calendars/version"
|
|
7
|
+
require_relative "beta_calendars/errors"
|
|
8
|
+
require_relative "beta_calendars/serializable"
|
|
9
|
+
|
|
10
|
+
module BetaCalendars
|
|
11
|
+
module CalendarValidation
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def validate_year!(year)
|
|
15
|
+
unless year.is_a?(Integer) && year.between?(1, 9999)
|
|
16
|
+
raise InvalidCalendarDateError, "year must be an integer from 1 to 9999"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate_month!(month)
|
|
21
|
+
unless month.is_a?(Integer) && month.between?(1, 12)
|
|
22
|
+
raise InvalidCalendarDateError, "month must be an integer from 1 to 12"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def normalize_week_start(value)
|
|
27
|
+
normalized = value.respond_to?(:to_sym) ? value.to_sym : value
|
|
28
|
+
return normalized if %i[monday sunday].include?(normalized)
|
|
29
|
+
|
|
30
|
+
raise InvalidWeekStartError, value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
require_relative "beta_calendars/day_cell"
|
|
36
|
+
require_relative "beta_calendars/month"
|
|
37
|
+
require_relative "beta_calendars/year"
|
|
38
|
+
require_relative "beta_calendars/blank_grid"
|
|
39
|
+
require_relative "beta_calendars/iso_week"
|
|
40
|
+
require_relative "beta_calendars/cli"
|
|
41
|
+
|
|
42
|
+
module BetaCalendars
|
|
43
|
+
class << self
|
|
44
|
+
# Build a local Gregorian month grid. Out-of-month cells contain adjacent
|
|
45
|
+
# dates by default; pass adjacent_dates: false to use nil cells instead.
|
|
46
|
+
# @param year [Integer] Gregorian year from 1 through 9999
|
|
47
|
+
# @param month [Integer] month number from 1 through 12
|
|
48
|
+
# @param week_start [Symbol, String] :monday or :sunday
|
|
49
|
+
# @param adjacent_dates [Boolean] populate cells from neighboring months
|
|
50
|
+
# @param fixed_six_weeks [Boolean] always create six rows
|
|
51
|
+
# @return [BetaCalendars::Month]
|
|
52
|
+
def month(year:, month:, week_start: :sunday, adjacent_dates: true, fixed_six_weeks: false)
|
|
53
|
+
Month.new(
|
|
54
|
+
year: year,
|
|
55
|
+
month: month,
|
|
56
|
+
week_start: week_start,
|
|
57
|
+
adjacent_dates: adjacent_dates,
|
|
58
|
+
fixed_six_weeks: fixed_six_weeks
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Build twelve monthly calendars for the selected Gregorian year.
|
|
63
|
+
# @param year [Integer] Gregorian year from 1 through 9999
|
|
64
|
+
# @param week_start [Symbol, String] :monday or :sunday
|
|
65
|
+
# @param adjacent_dates [Boolean] populate cells from neighboring months
|
|
66
|
+
# @param fixed_six_weeks [Boolean] always create six rows per month
|
|
67
|
+
# @return [BetaCalendars::Year]
|
|
68
|
+
def year(year:, week_start: :sunday, adjacent_dates: true, fixed_six_weeks: false)
|
|
69
|
+
Year.new(
|
|
70
|
+
year: year,
|
|
71
|
+
week_start: week_start,
|
|
72
|
+
adjacent_dates: adjacent_dates,
|
|
73
|
+
fixed_six_weeks: fixed_six_weeks
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Build an empty printable planning grid with seven weekday columns.
|
|
78
|
+
# @param rows [Integer] row count from 1 through 60
|
|
79
|
+
# @param week_start [Symbol, String] :monday or :sunday
|
|
80
|
+
# @return [BetaCalendars::BlankGrid]
|
|
81
|
+
def blank_grid(rows: 6, week_start: :sunday)
|
|
82
|
+
BlankGrid.new(rows: rows, week_start: week_start)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Return whether a Gregorian year has 366 days.
|
|
86
|
+
# @param year [Integer] Gregorian year from 1 through 9999
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def leap_year?(year)
|
|
89
|
+
CalendarValidation.validate_year!(year)
|
|
90
|
+
(year % 400).zero? || ((year % 4).zero? && !(year % 100).zero?)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Return the number of days in a Gregorian month.
|
|
94
|
+
# @param year [Integer] Gregorian year from 1 through 9999
|
|
95
|
+
# @param month [Integer] month number from 1 through 12
|
|
96
|
+
# @return [Integer]
|
|
97
|
+
def days_in_month(year, month)
|
|
98
|
+
CalendarValidation.validate_year!(year)
|
|
99
|
+
CalendarValidation.validate_month!(month)
|
|
100
|
+
Date.new(year, month, -1, Date::GREGORIAN).day
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Return the ISO week and ISO week-year for a Date, or for year/month/day.
|
|
104
|
+
# @param date_or_year [Date, Integer] a Date or Gregorian year
|
|
105
|
+
# @param month [Integer, nil] month number when a year is supplied
|
|
106
|
+
# @param day [Integer, nil] day of month when a year is supplied
|
|
107
|
+
# @return [BetaCalendars::ISOWeek]
|
|
108
|
+
def iso_week(date_or_year, month = nil, day = nil)
|
|
109
|
+
date = if date_or_year.is_a?(Date) && month.nil? && day.nil?
|
|
110
|
+
date_or_year
|
|
111
|
+
elsif date_or_year.is_a?(Integer) && month.is_a?(Integer) && day.is_a?(Integer)
|
|
112
|
+
CalendarValidation.validate_year!(date_or_year)
|
|
113
|
+
CalendarValidation.validate_month!(month)
|
|
114
|
+
unless Date.valid_date?(date_or_year, month, day, Date::GREGORIAN)
|
|
115
|
+
raise InvalidCalendarDateError, "day must be valid for the selected year and month"
|
|
116
|
+
end
|
|
117
|
+
Date.new(date_or_year, month, day, Date::GREGORIAN)
|
|
118
|
+
else
|
|
119
|
+
raise ArgumentError, "provide a Date or integer year, month, and day"
|
|
120
|
+
end
|
|
121
|
+
ISOWeek.new(week: date.cweek, year: date.cwyear)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Return the same metadata-rich two-dimensional grid exposed by Month#weeks.
|
|
125
|
+
# @return [Array<Array<BetaCalendars::DayCell, nil>>]
|
|
126
|
+
def month_matrix(year:, month:, week_start: :sunday, adjacent_dates: true, fixed_six_weeks: false)
|
|
127
|
+
self.month(
|
|
128
|
+
year: year,
|
|
129
|
+
month: month,
|
|
130
|
+
week_start: week_start,
|
|
131
|
+
adjacent_dates: adjacent_dates,
|
|
132
|
+
fixed_six_weeks: fixed_six_weeks
|
|
133
|
+
).weeks
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: beta_calendars
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mateo Pedersen
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '6'
|
|
22
|
+
type: :development
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '5.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '6'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: rake
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '12.3'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '14'
|
|
42
|
+
type: :development
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '12.3'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '14'
|
|
52
|
+
description: |
|
|
53
|
+
Beta Calendars Ruby generates Gregorian month and year calendar structures,
|
|
54
|
+
including configurable Sunday or Monday week starts, adjacent-month cells,
|
|
55
|
+
ISO week metadata, fixed six-week print grids, blank planning grids, JSON
|
|
56
|
+
serialization, and a small command-line interface. All calculations run
|
|
57
|
+
locally using Ruby's standard Date library.
|
|
58
|
+
email: []
|
|
59
|
+
executables:
|
|
60
|
+
- beta-calendars
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- CHANGELOG.md
|
|
65
|
+
- CODE_OF_CONDUCT.md
|
|
66
|
+
- CONTRIBUTING.md
|
|
67
|
+
- LICENSE.txt
|
|
68
|
+
- README.md
|
|
69
|
+
- Rakefile
|
|
70
|
+
- beta_calendars.gemspec
|
|
71
|
+
- exe/beta-calendars
|
|
72
|
+
- lib/beta_calendars.rb
|
|
73
|
+
- lib/beta_calendars/blank_grid.rb
|
|
74
|
+
- lib/beta_calendars/cli.rb
|
|
75
|
+
- lib/beta_calendars/day_cell.rb
|
|
76
|
+
- lib/beta_calendars/errors.rb
|
|
77
|
+
- lib/beta_calendars/iso_week.rb
|
|
78
|
+
- lib/beta_calendars/month.rb
|
|
79
|
+
- lib/beta_calendars/serializable.rb
|
|
80
|
+
- lib/beta_calendars/version.rb
|
|
81
|
+
- lib/beta_calendars/year.rb
|
|
82
|
+
homepage: https://www.betacalendars.com/
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
85
|
+
metadata:
|
|
86
|
+
homepage_uri: https://www.betacalendars.com/
|
|
87
|
+
source_code_uri: https://github.com/mateopedersen/beta-calendars-ruby
|
|
88
|
+
documentation_uri: https://github.com/mateopedersen/beta-calendars-ruby#readme
|
|
89
|
+
changelog_uri: https://github.com/mateopedersen/beta-calendars-ruby/blob/main/CHANGELOG.md
|
|
90
|
+
bug_tracker_uri: https://github.com/mateopedersen/beta-calendars-ruby/issues
|
|
91
|
+
rubygems_mfa_required: 'true'
|
|
92
|
+
rdoc_options: []
|
|
93
|
+
require_paths:
|
|
94
|
+
- lib
|
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '3.1'
|
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
requirements: []
|
|
106
|
+
rubygems_version: 4.0.20
|
|
107
|
+
specification_version: 4
|
|
108
|
+
summary: Deterministic Ruby calendar grids for monthly, yearly and printable calendar
|
|
109
|
+
applications.
|
|
110
|
+
test_files: []
|