timeplan_calculation.rb 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class TimeplanCalculation
  3. DAY_MAP = {
  4. 0 => 'Sun',
  5. 1 => 'Mon',
  6. 2 => 'Tue',
  7. 3 => 'Wed',
  8. 4 => 'Thu',
  9. 5 => 'Fri',
  10. 6 => 'Sat'
  11. }.freeze
  12. attr_reader :timeplan
  13. def initialize(timeplan, timezone)
  14. @timeplan = timeplan.deep_transform_keys(&:to_s)
  15. @timezone = timezone
  16. end
  17. # Checks if given time matches timeplan
  18. # @param [Time]
  19. # @return [Boolean]
  20. def contains?(time)
  21. return false if !valid?
  22. time_in_zone = ensure_matching_time(time)
  23. day?(time_in_zone) && hour?(time_in_zone) && minute?(time_in_zone)
  24. end
  25. # Calculates next time in timeplan after the given time
  26. # @param [Time]
  27. # @return [Time, nil]
  28. def next_at(time)
  29. return nil if !valid?
  30. time_in_zone = ensure_matching_time(time)
  31. next_run_at_same_day(time_in_zone) || next_run_at_coming_week(time_in_zone)
  32. end
  33. # Calculates previous time in timeplan before the given time
  34. # @param [Time]
  35. # @return [Time, nil]
  36. def previous_at(time)
  37. return nil if !valid?
  38. time_in_zone = ensure_matching_time(time)
  39. previous_run_at_same_day(time_in_zone) || previous_run_at_past_week(time_in_zone)
  40. end
  41. private
  42. def ensure_matching_time(time)
  43. time.in_time_zone @timezone
  44. end
  45. def valid?
  46. timeplan.key?('days') && timeplan.key?('hours') && timeplan.key?('minutes')
  47. end
  48. def match_minutes(minutes)
  49. minutes / 10 * 10
  50. end
  51. def day?(time)
  52. timeplan['days'][DAY_MAP[time.wday]]
  53. end
  54. def hour?(time)
  55. timeplan.dig 'hours', time.hour.to_s
  56. end
  57. def minute?(time)
  58. timeplan.dig 'minutes', match_minutes(time.min).to_s
  59. end
  60. def next_loop_minutes(base_time)
  61. loop_minutes base_time, range: 0.step(50, 10)
  62. end
  63. def previous_loop_minutes(base_time)
  64. loop_minutes base_time, range: 50.step(0, -10)
  65. end
  66. def loop_minutes(base_time, range:)
  67. return if !hour?(base_time)
  68. range
  69. .lazy
  70. .map { |minute| base_time.change min: minute }
  71. .find { |time| minute?(time) }
  72. end
  73. def next_loop_hours(base_time)
  74. loop_hours base_time, range: (base_time.hour..23), minutes_symbol: :next_loop_minutes
  75. end
  76. def previous_loop_hours(base_time)
  77. loop_hours base_time, range: (0..base_time.hour).entries.reverse, minutes_symbol: :previous_loop_minutes
  78. end
  79. def loop_hours(base_time, range:, minutes_symbol:)
  80. return if !day?(base_time)
  81. range
  82. .lazy
  83. .map { |hour| send(minutes_symbol, base_time.change(hour: hour)) }
  84. .find(&:present?)
  85. end
  86. def next_loop_partial_hour(base_time)
  87. loop_partial_hour base_time, range: base_time.min.step(50, 10)
  88. end
  89. def previous_loop_partial_hour(base_time)
  90. loop_partial_hour base_time, range: base_time.min.step(0, -10)
  91. end
  92. def loop_partial_hour(base_time, range:)
  93. return if !day?(base_time)
  94. range
  95. .lazy
  96. .map { |minute| base_time.change(min: minute) }
  97. .find { |time| hour?(time) && minute?(time) }
  98. end
  99. def next_run_at_same_day(time)
  100. day_to_check = time.change min: match_minutes(time.min)
  101. if day_to_check.min.nonzero?
  102. date = next_loop_partial_hour(day_to_check)
  103. return date if date
  104. day_to_check = day_to_check.change(min: 0)
  105. day_to_check += 1.hour
  106. end
  107. next_loop_hours(day_to_check)
  108. end
  109. def next_run_at_coming_week(time)
  110. (1..7)
  111. .lazy
  112. .map { |day| next_loop_hours (time + day.day).beginning_of_day }
  113. .find(&:present?)
  114. end
  115. def previous_run_at_same_day(time)
  116. day_to_check = time.change min: match_minutes(time.min)
  117. if day_to_check.min.nonzero?
  118. date = previous_loop_partial_hour(day_to_check)
  119. return date if date
  120. day_to_check = day_to_check.change(min: 0)
  121. day_to_check -= 1.second
  122. end
  123. previous_loop_hours(day_to_check)
  124. end
  125. def previous_run_at_past_week(time)
  126. (1..7)
  127. .lazy
  128. .map { |day| previous_loop_hours (time - day.day).end_of_day }
  129. .find(&:present?)
  130. end
  131. end