verified_holidays 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 +14 -0
- data/LICENSE +21 -0
- data/README.md +110 -0
- data/data/holidays.yml +1068 -0
- data/lib/verified_holidays/cabinet_office.rb +44 -0
- data/lib/verified_holidays/dataset.rb +63 -0
- data/lib/verified_holidays/holiday.rb +51 -0
- data/lib/verified_holidays/holiday_jp_compat.rb +10 -0
- data/lib/verified_holidays/verifier.rb +63 -0
- data/lib/verified_holidays/version.rb +5 -0
- data/lib/verified_holidays.rb +29 -0
- metadata +72 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'csv'
|
|
6
|
+
require 'date'
|
|
7
|
+
|
|
8
|
+
module VerifiedHolidays
|
|
9
|
+
class CabinetOffice
|
|
10
|
+
CSV_URL = 'https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv'
|
|
11
|
+
|
|
12
|
+
def self.fetch
|
|
13
|
+
uri = URI.parse(CSV_URL)
|
|
14
|
+
response = Net::HTTP.get(uri)
|
|
15
|
+
|
|
16
|
+
# Remove UTF-8 BOM bytes if present (before encoding conversion)
|
|
17
|
+
raw = response.b
|
|
18
|
+
raw = raw.byteslice(3..) if raw.byteslice(0, 3) == "\xEF\xBB\xBF".b
|
|
19
|
+
|
|
20
|
+
# Convert from CP932 (Shift_JIS) to UTF-8
|
|
21
|
+
utf8 = raw.force_encoding('CP932').encode('UTF-8')
|
|
22
|
+
|
|
23
|
+
parse(utf8)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.parse(csv_string)
|
|
27
|
+
holidays = {}
|
|
28
|
+
CSV.parse(csv_string, headers: true) do |row|
|
|
29
|
+
date_str = row[0]&.strip
|
|
30
|
+
name = row[1]&.strip
|
|
31
|
+
next if date_str.nil? || name.nil?
|
|
32
|
+
|
|
33
|
+
holidays[parse_date(date_str)] = name
|
|
34
|
+
end
|
|
35
|
+
holidays
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.parse_date(date_str)
|
|
39
|
+
parts = date_str.split('/')
|
|
40
|
+
Date.new(parts[0].to_i, parts[1].to_i, parts[2].to_i)
|
|
41
|
+
end
|
|
42
|
+
private_class_method :parse_date
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'date'
|
|
6
|
+
|
|
7
|
+
module VerifiedHolidays
|
|
8
|
+
class Dataset
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@holidays = load_yaml
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all
|
|
16
|
+
@holidays
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def between(start_date, last_date)
|
|
20
|
+
start_date = to_date(start_date)
|
|
21
|
+
last_date = to_date(last_date)
|
|
22
|
+
@holidays.each_with_object([]) do |(date, holiday), result|
|
|
23
|
+
result << holiday if date.between?(start_date, last_date)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def holiday?(date)
|
|
28
|
+
@holidays.key?(to_date(date))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def name(date)
|
|
32
|
+
holiday = @holidays[to_date(date)]
|
|
33
|
+
holiday&.name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def year(year)
|
|
37
|
+
@holidays.each_with_object([]) do |(date, holiday), result|
|
|
38
|
+
result << holiday if date.year == year
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def to_date(value)
|
|
45
|
+
# DateTime は Date のサブクラスだが、明示的に .to_date で Date に変換
|
|
46
|
+
return value.to_date if value.is_a?(DateTime) || value.is_a?(Time)
|
|
47
|
+
return value if value.is_a?(Date)
|
|
48
|
+
|
|
49
|
+
raise ArgumentError, "expected Date, DateTime, or Time, got #{value.class}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def load_yaml
|
|
53
|
+
path = File.expand_path('../../data/holidays.yml', __dir__)
|
|
54
|
+
data = YAML.safe_load_file(path, permitted_classes: [Date])
|
|
55
|
+
result = {}
|
|
56
|
+
data.each do |date, name|
|
|
57
|
+
date = Date.parse(date.to_s) unless date.is_a?(Date)
|
|
58
|
+
result[date] = Holiday.new(date, name)
|
|
59
|
+
end
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifiedHolidays
|
|
4
|
+
class Holiday
|
|
5
|
+
WDAY_NAMES = %w[日 月 火 水 木 金 土].freeze
|
|
6
|
+
|
|
7
|
+
EN_HOLIDAY_NAMES = {
|
|
8
|
+
'元日' => "New Year's Day",
|
|
9
|
+
'成人の日' => 'Coming of Age Day',
|
|
10
|
+
'建国記念の日' => 'National Foundation Day',
|
|
11
|
+
'天皇誕生日' => "Emperor's Birthday",
|
|
12
|
+
'春分の日' => 'Vernal Equinox Day',
|
|
13
|
+
'昭和の日' => 'Showa Day',
|
|
14
|
+
'憲法記念日' => 'Constitution Memorial Day',
|
|
15
|
+
'みどりの日' => 'Greenery Day',
|
|
16
|
+
'こどもの日' => "Children's Day",
|
|
17
|
+
'海の日' => 'Marine Day',
|
|
18
|
+
'山の日' => 'Mountain Day',
|
|
19
|
+
'敬老の日' => 'Respect for the Aged Day',
|
|
20
|
+
'秋分の日' => 'Autumnal Equinox Day',
|
|
21
|
+
'スポーツの日' => 'Sports Day',
|
|
22
|
+
'文化の日' => 'Culture Day',
|
|
23
|
+
'勤労感謝の日' => 'Labor Thanksgiving Day',
|
|
24
|
+
'振替休日' => 'Substitute Holiday',
|
|
25
|
+
'休日' => "Citizens' Holiday",
|
|
26
|
+
'体育の日' => 'Health and Sports Day',
|
|
27
|
+
'国民の休日' => "Citizens' Holiday",
|
|
28
|
+
'即位礼正殿の儀の行われる日' => 'Enthronement Ceremony',
|
|
29
|
+
'即位の日' => 'Enthronement Day',
|
|
30
|
+
'天皇の即位の日及び即位礼正殿の儀の行われる日を休日とする法律' => 'Holiday by Law',
|
|
31
|
+
'みどりの日(国民の休日)' => "Greenery Day (Citizens' Holiday)",
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
attr_reader :date, :name
|
|
35
|
+
|
|
36
|
+
def initialize(date, name)
|
|
37
|
+
@date = date
|
|
38
|
+
@name = name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def name_en
|
|
42
|
+
EN_HOLIDAY_NAMES[name]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def wday_name
|
|
46
|
+
WDAY_NAMES[date.wday]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
alias week wday_name
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'verified_holidays'
|
|
4
|
+
|
|
5
|
+
# Drop-in replacement for holiday_jp gem.
|
|
6
|
+
# Simply replace:
|
|
7
|
+
# require 'holiday_jp'
|
|
8
|
+
# with:
|
|
9
|
+
# require 'verified_holidays/holiday_jp_compat'
|
|
10
|
+
HolidayJp = VerifiedHolidays unless defined?(HolidayJp)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifiedHolidays
|
|
4
|
+
class Verifier
|
|
5
|
+
class Result
|
|
6
|
+
attr_reader :missing, :extra, :mismatched
|
|
7
|
+
|
|
8
|
+
def initialize(missing:, extra:, mismatched:)
|
|
9
|
+
@missing = missing
|
|
10
|
+
@extra = extra
|
|
11
|
+
@mismatched = mismatched
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?
|
|
15
|
+
missing.empty? && extra.empty? && mismatched.empty?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.verify!
|
|
20
|
+
cabinet_data = CabinetOffice.fetch
|
|
21
|
+
local_in_range = filter_local_data(cabinet_data)
|
|
22
|
+
|
|
23
|
+
missing, mismatched = compare_local_to_cabinet(local_in_range, cabinet_data)
|
|
24
|
+
extra = find_extra(cabinet_data, local_in_range)
|
|
25
|
+
|
|
26
|
+
Result.new(missing: missing, extra: extra, mismatched: mismatched)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.filter_local_data(cabinet_data)
|
|
30
|
+
cabinet_dates = cabinet_data.keys
|
|
31
|
+
min_date = cabinet_dates.min
|
|
32
|
+
max_date = cabinet_dates.max
|
|
33
|
+
|
|
34
|
+
Dataset.instance.all.select { |date, _| date.between?(min_date, max_date) }
|
|
35
|
+
end
|
|
36
|
+
private_class_method :filter_local_data
|
|
37
|
+
|
|
38
|
+
def self.compare_local_to_cabinet(local_in_range, cabinet_data)
|
|
39
|
+
missing = []
|
|
40
|
+
mismatched = []
|
|
41
|
+
|
|
42
|
+
local_in_range.each do |date, holiday|
|
|
43
|
+
unless cabinet_data.key?(date)
|
|
44
|
+
missing << { date: date, name: holiday.name }
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
cabinet_name = cabinet_data[date]
|
|
49
|
+
mismatched << { date: date, expected: holiday.name, actual: cabinet_name } if holiday.name != cabinet_name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[missing, mismatched]
|
|
53
|
+
end
|
|
54
|
+
private_class_method :compare_local_to_cabinet
|
|
55
|
+
|
|
56
|
+
def self.find_extra(cabinet_data, local_in_range)
|
|
57
|
+
cabinet_data.each_with_object([]) do |(date, name), extra|
|
|
58
|
+
extra << { date: date, name: name } unless local_in_range.key?(date)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
private_class_method :find_extra
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'verified_holidays/version'
|
|
4
|
+
require_relative 'verified_holidays/holiday'
|
|
5
|
+
require_relative 'verified_holidays/dataset'
|
|
6
|
+
require_relative 'verified_holidays/cabinet_office'
|
|
7
|
+
require_relative 'verified_holidays/verifier'
|
|
8
|
+
|
|
9
|
+
module VerifiedHolidays
|
|
10
|
+
def self.holiday?(date)
|
|
11
|
+
Dataset.instance.holiday?(date)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.between(start_date, last_date)
|
|
15
|
+
Dataset.instance.between(start_date, last_date)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.name(date)
|
|
19
|
+
Dataset.instance.name(date)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.year(year)
|
|
23
|
+
Dataset.instance.year(year)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.verify!
|
|
27
|
+
Verifier.verify!
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: verified_holidays
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kenta Ishizaki
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: csv
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: A Japanese national holiday gem that verifies its built-in data against
|
|
27
|
+
the Cabinet Office official CSV every week. Provides holiday?, between, name, and
|
|
28
|
+
year methods with English name support.
|
|
29
|
+
email:
|
|
30
|
+
- kentaishizaki@55728.jp
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- data/holidays.yml
|
|
39
|
+
- lib/verified_holidays.rb
|
|
40
|
+
- lib/verified_holidays/cabinet_office.rb
|
|
41
|
+
- lib/verified_holidays/dataset.rb
|
|
42
|
+
- lib/verified_holidays/holiday.rb
|
|
43
|
+
- lib/verified_holidays/holiday_jp_compat.rb
|
|
44
|
+
- lib/verified_holidays/verifier.rb
|
|
45
|
+
- lib/verified_holidays/version.rb
|
|
46
|
+
homepage: https://github.com/55728/verified_holidays
|
|
47
|
+
licenses:
|
|
48
|
+
- MIT
|
|
49
|
+
metadata:
|
|
50
|
+
homepage_uri: https://github.com/55728/verified_holidays
|
|
51
|
+
source_code_uri: https://github.com/55728/verified_holidays
|
|
52
|
+
changelog_uri: https://github.com/55728/verified_holidays/blob/main/CHANGELOG.md
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.1'
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 4.0.6
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Japanese national holidays with weekly verification against Cabinet Office
|
|
71
|
+
data
|
|
72
|
+
test_files: []
|