holiday_co 1.0.1 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9166d0bbc45b36c157119cef9469f17d8974651c1bde43dd53c918d026534404
4
- data.tar.gz: 5c0099db1b7377200a2f3bfdc81130e3609ac99d978c84400311ce135fcf757b
3
+ metadata.gz: d26e0ed39b140dc05c8d82ef874b7793ebe17b4215c60c216f9c3b7a3afa9481
4
+ data.tar.gz: 97f70e6d2aae69a87054a402e9450a40e5c2a263d4dac2b37bb129f47a8b1873
5
5
  SHA512:
6
- metadata.gz: 22dbf800d65123ff86e12e93da9ec72b629faa90b7d9cd2fe4e587bd1169762921786c353ecd782ed5a756c995a57e28114b5781ecfc62bb9440ec1807f30497
7
- data.tar.gz: a8e16af3be4156af15dfdb1ae01e2817ea8c01b56f9faf5bb6dd8c59cae081685f67a74b5a5963f37749b380693362eeb2641d592deb2e1fe15f9afcb1cbe402
6
+ metadata.gz: e8640a73dfec13370288daaedf5730c5edcb7e85392417aab1a657f18ceb5c189ca385a1b2211cbd2b0f7d83b8aaba453fca3075be603c78fa618b74a5f50388
7
+ data.tar.gz: ba25e51996ae422b4eac1a34eb5c2c8001d515f0455aa505e4535b0425f412f3f02ce0942fe9931afed1dadad3bdfdf42f8181bd270c5f310dcbbe7768fbf683
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HolidayCo
4
+ # A tiny thread-safe LRU cache used to memoize per-year holiday calculations,
5
+ # so holidays are only ever calculated once per year per process.
6
+ class Cache
7
+ DEFAULT_MAX_SIZE = 64
8
+
9
+ attr_reader :max_size
10
+
11
+ def initialize(max_size: DEFAULT_MAX_SIZE)
12
+ raise ArgumentError, "max_size must be a positive integer" unless max_size.is_a?(Integer) && max_size.positive?
13
+
14
+ @max_size = max_size
15
+ @store = {}
16
+ @mutex = Mutex.new
17
+ end
18
+
19
+ def fetch(key)
20
+ @mutex.synchronize do
21
+ if @store.key?(key)
22
+ # Re-insert on hit so insertion order tracks recency.
23
+ @store[key] = @store.delete(key)
24
+ else
25
+ value = yield
26
+ @store[key] = value
27
+ @store.shift while @store.size > @max_size
28
+ value
29
+ end
30
+ end
31
+ end
32
+
33
+ def size
34
+ @mutex.synchronize { @store.size }
35
+ end
36
+
37
+ def clear!
38
+ @mutex.synchronize { @store.clear }
39
+ nil
40
+ end
41
+ end
42
+
43
+ class << self
44
+ attr_writer :cache_enabled
45
+ attr_reader :cache_size
46
+
47
+ def configure
48
+ yield self if block_given?
49
+ end
50
+
51
+ def cache_enabled?
52
+ @cache_enabled
53
+ end
54
+
55
+ def cache_size=(size)
56
+ @cache_size = size
57
+ @cache = nil
58
+ end
59
+
60
+ def cache
61
+ @cache ||= Cache.new(max_size: cache_size)
62
+ end
63
+
64
+ def clear_cache!
65
+ cache.clear!
66
+ end
67
+ end
68
+
69
+ @cache_enabled = true
70
+ @cache_size = Cache::DEFAULT_MAX_SIZE
71
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Dates are in YYYY-MM-DD format
4
+ module HolidayCo
5
+ module CalculateHolidays
6
+ class Fixed
7
+ FIXED_HOLIDAYS = {
8
+ "Año Nuevo" => "%4s-01-01",
9
+ "Día del trabajo" => "%4s-05-01",
10
+ "Día de la independencia" => "%4s-07-20",
11
+ "Batalla de Boyacá" => "%4s-08-07",
12
+ "Inmaculada Concepción" => "%4s-12-08",
13
+ "Navidad" => "%4s-12-25"
14
+ }
15
+
16
+ def self.for(year)
17
+ FIXED_HOLIDAYS.map do |holiday, date|
18
+ {
19
+ :name => holiday,
20
+ :date => date % year
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module NextMondayRefinement
6
+ refine Date do
7
+ def next_monday
8
+ from_now = 1 - wday
9
+ from_now += 7 unless from_now > 0
10
+ self + from_now
11
+ end
12
+ end
13
+ end
14
+
15
+ # Dates are in YYYY-MM-DD format
16
+ module HolidayCo
17
+ module CalculateHolidays
18
+ class Movable
19
+ using NextMondayRefinement
20
+
21
+ # Holidays that are moved to the following Monday
22
+ # if they don"t fall on a Monday.
23
+ MOVABLE_HOLIDAYS = {
24
+ "Epifanía" => "%4s-01-06",
25
+ "Día de San José" => "%4s-03-19",
26
+ "San Pedro y San Pablo" => "%4s-06-29",
27
+ "Día de la Virgen del Rosario de Chiquinquirá" => "%4s-07-09",
28
+ "Asunción de la Virgen" => "%4s-08-15",
29
+ "Día de la raza" => "%4s-10-12",
30
+ "Todos los Santos" => "%4s-11-01",
31
+ "Independencia de Cartagena" => "%4s-11-11"
32
+ }
33
+
34
+ # Holidays introduced after 1983, keyed by the first year they apply.
35
+ # Ley 2578 de 2026 declared the Día de la Virgen del Rosario de Chiquinquirá.
36
+ EFFECTIVE_SINCE = {
37
+ "Día de la Virgen del Rosario de Chiquinquirá" => 2026
38
+ }
39
+
40
+ def self.for(year)
41
+ MOVABLE_HOLIDAYS.filter_map do |holiday, date|
42
+ next if year.to_i < EFFECTIVE_SINCE.fetch(holiday, 0)
43
+
44
+ day = Date.parse(date % year)
45
+ date = day.monday? ? day : day.next_monday
46
+ {
47
+ :name => holiday,
48
+ :date => date.to_s
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require_relative "../utils"
5
+
6
+ # Dates are in YYYY-MM-DD format
7
+ module HolidayCo
8
+ module CalculateHolidays
9
+ class Pascua
10
+ PASCUA_HOLIDAYS = {
11
+ "Jueves Santo" => {
12
+ operation: :-,
13
+ days: 3
14
+ },
15
+ "Viernes Santo" => {
16
+ operation: :-,
17
+ days: 2
18
+ },
19
+ "Ascención de Jesús" => {
20
+ operation: :+,
21
+ days: 43
22
+ },
23
+ "Corpus Christi" => {
24
+ operation: :+,
25
+ days: 64
26
+ },
27
+ "Sagrado Corazón de Jesús" => {
28
+ operation: :+,
29
+ days: 71
30
+ }
31
+ }
32
+
33
+ def self.for(year)
34
+ pascua_day = Date.parse(HolidayCo.pascua_day_per_year(year.to_i)[year.to_s])
35
+ PASCUA_HOLIDAYS.map do |holiday, ops|
36
+ {
37
+ :name => holiday,
38
+ :date => pascua_day.public_send(ops[:operation], ops[:days]).to_s
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HolidayCo
4
+ class YearDataNotAvailableError < StandardError
5
+ def message
6
+ "Colombian holidays can only be calculated for years #{AVAILABLE_YEARS.first} through #{AVAILABLE_YEARS.last}."
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cache'
4
+ require_relative 'calculate_holidays/fixed'
5
+ require_relative 'calculate_holidays/movable'
6
+ require_relative 'calculate_holidays/pascua'
7
+
8
+ # Source of Calculation Rules: https://www.festivos.com.co/calculo
9
+ module HolidayCo
10
+ class HolidayCalculator
11
+ attr_reader :year
12
+
13
+ def self.for(year)
14
+ year = year.to_i
15
+ return new(year).calculate unless HolidayCo.cache_enabled?
16
+
17
+ HolidayCo.cache.fetch(year) { new(year).calculate }
18
+ end
19
+
20
+ def initialize(year)
21
+ @year = year
22
+ end
23
+
24
+ # Results are deep-frozen: they are shared through the cache across
25
+ # callers and threads, so they must be immutable.
26
+ def calculate
27
+ [fixed_holidays, pascua_holidays, movable_holidays]
28
+ .flatten
29
+ .sort_by! { |h| h[:date] }
30
+ .each { |h| h[:date].freeze; h.freeze }
31
+ .freeze
32
+ end
33
+
34
+ private
35
+
36
+ def fixed_holidays
37
+ HolidayCo::CalculateHolidays::Fixed.for(year)
38
+ end
39
+
40
+ def pascua_holidays
41
+ HolidayCo::CalculateHolidays::Pascua.for(year)
42
+ end
43
+
44
+ def movable_holidays
45
+ HolidayCo::CalculateHolidays::Movable.for(year)
46
+ end
47
+ end
48
+ end
@@ -1,14 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
3
+ require_relative "../errors"
4
+ require_relative "../holiday_calculator"
4
5
 
5
6
  module HolidayCo
6
- class YearDataNotAvailableError < StandardError
7
- def message
8
- "There is no data file available for the specified year (yet)."
9
- end
10
- end
11
-
12
7
  class Year
13
8
  attr_reader :year
14
9
 
@@ -19,10 +14,7 @@ module HolidayCo
19
14
  def holidays
20
15
  raise YearDataNotAvailableError unless year_data_available?
21
16
 
22
- YAML
23
- .load_file(File.expand_path("../../../../data/years/#{year}.yml", __FILE__))
24
- .fetch("holidays", [])
25
- .map { |h| h.transform_keys(&:to_sym) }
17
+ HolidayCalculator.for(year)
26
18
  end
27
19
 
28
20
  def holiday_names
@@ -36,13 +28,11 @@ module HolidayCo
36
28
  private
37
29
 
38
30
  def year_data_available?
39
- available_years.include?(year)
31
+ available_years.include?(year.to_i)
40
32
  end
41
33
 
42
34
  def available_years
43
- YAML
44
- .load_file(File.expand_path("../../../../data/years.yml", __FILE__))
45
- .map(&:to_s)
35
+ HolidayCo::AVAILABLE_YEARS
46
36
  end
47
37
  end
48
38
  end
@@ -1,9 +1,13 @@
1
1
  require "date"
2
+ require_relative "errors"
2
3
 
3
4
  module HolidayCo
4
5
  # Colombian time zone is 5 hours behind UTC.
5
6
  UTC_OFFSET = "-05:00".freeze
6
7
 
8
+ # Colombian Holidays started in 1983
9
+ AVAILABLE_YEARS = (1983..9999).freeze
10
+
7
11
  def self.current_time
8
12
  Time.now.getlocal(UTC_OFFSET)
9
13
  end
@@ -15,4 +19,70 @@ module HolidayCo
15
19
  def self.current_date
16
20
  current_time.to_date
17
21
  end
22
+
23
+ def self.pascua_day_per_year(year)
24
+ raise YearDataNotAvailableError unless AVAILABLE_YEARS.cover?(year)
25
+
26
+ calculate_pascua_day_per_year(year)
27
+ end
28
+
29
+ private
30
+
31
+ def self.calculate_pascua_day_per_year(year)
32
+ # The "Día de Pascua" (aka Domingo de Resurrección) plays a crucial role in determining
33
+ # 5 of the 18 holidays, but its calculation is unique. For more details, check out:
34
+ # https://blogs.elespectador.com/actualidad/algoritmo-calcular-las-fechas-la-semana-santa-ano
35
+ # This calculation is based on an astronomical algorithm that takes into account the lunar cycle,
36
+ # leap years, and the position of weekdays, which is used to calculate the exact date of
37
+ # pascua each year
38
+
39
+ lunar_cycle_position = year % 19 # a: Position in the lunar cycle
40
+ leap_year_indicator = year % 4 # b: Leap year indicator
41
+ week_day_position = year % 7 # c: Day of the week
42
+
43
+ century = year / 100 # d: Century of the year
44
+ leap_year_adjustment = (13 + 8 * century) / 25 # e: Adjustment for leap years in centuries
45
+ century_quarters = century / 4 # m: Adjustment for century quarters
46
+
47
+ lunar_adjustment = (15 - leap_year_adjustment + century - century_quarters) % 30 # n: Lunar adjustment for Easter date
48
+ weekday_adjustment = (4 + century - century_quarters) % 7 # p: Adjustment for weekday of March 22
49
+
50
+ march_22_offset = (19 * lunar_cycle_position + lunar_adjustment) % 30 # q: Lunar cycle position and adjustment
51
+
52
+ final_weekday_adjustment = (
53
+ (2 * leap_year_indicator) +
54
+ (4 * week_day_position) +
55
+ (6 * march_22_offset) +
56
+ weekday_adjustment
57
+ ) % 7 # r: Final weekday adjustment
58
+
59
+ pascua_offset = march_22_offset + final_weekday_adjustment
60
+ day, month = calculate_pascua_offset(pascua_offset)
61
+ day = adjust_day_for_pascua(day, month, march_22_offset, final_weekday_adjustment, lunar_cycle_position)
62
+
63
+ generate_pascua_date(year, month, day)
64
+ end
65
+
66
+ def self.calculate_pascua_offset(pascua_offset)
67
+ pascua_offset <= 9 ? [pascua_offset + 22, "03"] : [pascua_offset - 9, "04"]
68
+ end
69
+
70
+ def self.exception_date?(day, month)
71
+ [26, 25].include?(day) && month == "04"
72
+ end
73
+
74
+ def self.pascua_correction?(day, march_22_offset, days_to_sunday, lunar_cycle_position)
75
+ day == 26 || march_22_offset == 28 && days_to_sunday == 6 && lunar_cycle_position > 10
76
+ end
77
+
78
+ def self.adjust_day_for_pascua(day, month, march_22_offset, days_to_sunday, lunar_cycle_position)
79
+ return day unless exception_date?(day, month)
80
+ return day unless pascua_correction?(day, march_22_offset, days_to_sunday, lunar_cycle_position)
81
+
82
+ day -= 7
83
+ end
84
+
85
+ def self.generate_pascua_date(year, month, day)
86
+ { "#{year}" => "#{year}-#{month}-#{format('%02d', day)}" }
87
+ end
18
88
  end
@@ -1,3 +1,3 @@
1
1
  module HolidayCo
2
- VERSION = "1.0.1"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/holiday_co.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "holiday_co/errors"
4
+ require_relative "holiday_co/cache"
3
5
  require_relative "holiday_co/utils"
4
6
  require_relative "holiday_co/version"
5
7
  require_relative "holiday_co/models/day"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: holiday_co
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Coronado
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-30 00:00:00.000000000 Z
11
+ date: 2026-07-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A simple gem to handle holidays in Colombia
14
14
  email:
@@ -17,12 +17,13 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - data/years.yml
21
- - data/years/2023.yml
22
- - data/years/2024.yml
23
- - data/years/2025.yml
24
- - data/years/2026.yml
25
20
  - lib/holiday_co.rb
21
+ - lib/holiday_co/cache.rb
22
+ - lib/holiday_co/calculate_holidays/fixed.rb
23
+ - lib/holiday_co/calculate_holidays/movable.rb
24
+ - lib/holiday_co/calculate_holidays/pascua.rb
25
+ - lib/holiday_co/errors.rb
26
+ - lib/holiday_co/holiday_calculator.rb
26
27
  - lib/holiday_co/models/day.rb
27
28
  - lib/holiday_co/models/year.rb
28
29
  - lib/holiday_co/utils.rb
@@ -39,7 +40,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
39
40
  requirements:
40
41
  - - ">="
41
42
  - !ruby/object:Gem::Version
42
- version: '0'
43
+ version: '3.0'
43
44
  required_rubygems_version: !ruby/object:Gem::Requirement
44
45
  requirements:
45
46
  - - ">="
data/data/years/2023.yml DELETED
@@ -1,39 +0,0 @@
1
- description: Lista de festivos Colombianos 2023
2
- source: https://www.festivos.com.co/
3
- holidays:
4
- - name: Año Nuevo
5
- date: '2023-01-01'
6
- - name: Epifanía
7
- date: '2023-01-09'
8
- - name: Día de San José
9
- date: '2023-03-20'
10
- - name: Jueves Santo
11
- date: '2023-04-06'
12
- - name: Viernes Santo
13
- date: '2023-04-07'
14
- - name: Día del trabajo
15
- date: '2023-05-01'
16
- - name: Ascención de Jesús
17
- date: '2023-05-22'
18
- - name: Corpus Christi
19
- date: '2023-06-12'
20
- - name: Sagrado Corazón de Jesús
21
- date: '2023-06-19'
22
- - name: San Pedro y San Pablo
23
- date: '2023-07-03'
24
- - name: Día de la independencia
25
- date: '2023-07-20'
26
- - name: Batalla de Boyacá
27
- date: '2023-08-07'
28
- - name: Asunción de la Virgen
29
- date: '2023-08-21'
30
- - name: Día de la raza
31
- date: '2023-10-16'
32
- - name: Todos los Santos
33
- date: '2023-11-06'
34
- - name: Independencia de Cartagena
35
- date: '2023-11-13'
36
- - name: Inmaculada Concepción
37
- date: '2023-12-08'
38
- - name: Navidad
39
- date: '2023-12-25'
data/data/years/2024.yml DELETED
@@ -1,39 +0,0 @@
1
- description: Lista de festivos Colombianos 2024
2
- source: https://www.festivos.com.co/
3
- holidays:
4
- - name: Año Nuevo
5
- date: '2024-01-01'
6
- - name: Epifanía
7
- date: '2024-01-08'
8
- - name: Día de San José
9
- date: '2024-03-25'
10
- - name: Jueves Santo
11
- date: '2024-03-28'
12
- - name: Viernes Santo
13
- date: '2024-03-29'
14
- - name: Día del trabajo
15
- date: '2024-05-01'
16
- - name: Ascención de Jesús
17
- date: '2024-05-13'
18
- - name: Corpus Christi
19
- date: '2024-06-03'
20
- - name: Sagrado Corazón de Jesús
21
- date: '2024-06-10'
22
- - name: San Pedro y San Pablo
23
- date: '2024-07-01'
24
- - name: Día de la independencia
25
- date: '2024-07-20'
26
- - name: Batalla de Boyacá
27
- date: '2024-08-07'
28
- - name: Asunción de la Virgen
29
- date: '2024-08-19'
30
- - name: Día de la raza
31
- date: '2024-10-14'
32
- - name: Todos los Santos
33
- date: '2024-11-04'
34
- - name: Independencia de Cartagena
35
- date: '2024-11-11'
36
- - name: Inmaculada Concepción
37
- date: '2024-12-08'
38
- - name: Navidad
39
- date: '2024-12-25'
data/data/years/2025.yml DELETED
@@ -1,43 +0,0 @@
1
- description: Lista de festivos Colombianos 2025
2
- source: https://www.festivos.com.co/
3
- holidays:
4
- - name: Año Nuevo
5
- date: '2024-01-01'
6
- - name: Epifanía
7
- date: '2024-01-08'
8
- - name: Año Nuevo
9
- date: '2025-01-01'
10
- - name: Epifanía
11
- date: '2025-01-06'
12
- - name: Día de San José
13
- date: '2025-03-24'
14
- - name: Jueves Santo
15
- date: '2025-04-17'
16
- - name: Viernes Santo
17
- date: '2025-04-18'
18
- - name: Día del trabajo
19
- date: '2025-05-01'
20
- - name: Ascención de Jesús
21
- date: '2025-06-02'
22
- - name: Corpus Christi
23
- date: '2025-06-23'
24
- - name: Sagrado Corazón de Jesús
25
- date: '2025-06-30'
26
- - name: San Pedro y San Pablo
27
- date: '2025-06-30'
28
- - name: Día de la independencia
29
- date: '2025-07-20'
30
- - name: Batalla de Boyacá
31
- date: '2025-08-07'
32
- - name: Asunción de la Virgen
33
- date: '2025-08-18'
34
- - name: Día de la raza
35
- date: '2025-10-13'
36
- - name: Todos los Santos
37
- date: '2025-11-03'
38
- - name: Independencia de Cartagena
39
- date: '2025-11-17'
40
- - name: Inmaculada Concepción
41
- date: '2025-12-08'
42
- - name: Navidad
43
- date: '2025-12-25'
data/data/years/2026.yml DELETED
@@ -1,39 +0,0 @@
1
- description: Lista de festivos Colombianos 2026
2
- source: https://www.festivos.com.co/
3
- holidays:
4
- - name: Año Nuevo
5
- date: '2026-01-01'
6
- - name: Reyes Magos
7
- date: '2026-01-12'
8
- - name: Día de San José
9
- date: '2026-03-23'
10
- - name: Jueves Santo
11
- date: '2026-04-02'
12
- - name: Viernes Santo
13
- date: '2026-04-03'
14
- - name: Día del Trabajo
15
- date: '2026-05-01'
16
- - name: Ascensión de Jesús
17
- date: '2026-05-18'
18
- - name: Corpus Christi
19
- date: '2026-06-08'
20
- - name: Sagrado Corazón de Jesús
21
- date: '2026-06-15'
22
- - name: San Pedro y San Pablo
23
- date: '2026-06-29'
24
- - name: Día de la Independencia
25
- date: '2026-07-20'
26
- - name: Batalla de Boyacá
27
- date: '2026-08-07'
28
- - name: Asunción de la Virgen
29
- date: '2026-08-17'
30
- - name: Día de la Raza
31
- date: '2026-10-12'
32
- - name: Todos los Santos
33
- date: '2026-11-02'
34
- - name: Independencia de Cartagena
35
- date: '2026-11-16'
36
- - name: Inmaculada Concepción
37
- date: '2026-12-08'
38
- - name: Navidad
39
- date: '2026-12-25'
data/data/years.yml DELETED
@@ -1,6 +0,0 @@
1
- # List any new supported year here (in addition to adding the .yml in years)
2
-
3
- - 2023
4
- - 2024
5
- - 2025
6
- - 2026