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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VerifiedHolidays
4
+ VERSION = '0.1.0'
5
+ 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: []