Module: Menuconform::Rules::AvailRules
- Defined in:
- lib/menuconform/rules/avail_rules.rb
Overview
AVAIL-001..003. Weekly-window math: windows expand to minute intervals over a 10080-minute week; end < start wraps past midnight; '24:00' is end-of-day. Date bounds (start_date/end_date) are ignored for overlap checks in this release — AVAIL-001 reasons about the recurring weekly pattern only.
Constant Summary collapse
- DAYS =
%w[mon tue wed thu fri sat sun].freeze
- WEEK_MINUTES =
7 * 1440
- SUSPENSION_HORIZON_SECONDS =
365 * 24 * 3600
Class Method Summary collapse
- .call(menu) ⇒ Object
- .contradictions(menu) ⇒ Object
- .declared_never(menu) ⇒ Object
- .indefinite_suspensions(menu) ⇒ Object
- .intervals(windows) ⇒ Object
- .minute_of_day(hhmm) ⇒ Object
- .overlap?(windows_a, windows_b) ⇒ Boolean
- .present?(windows) ⇒ Boolean
Class Method Details
.call(menu) ⇒ Object
18 19 20 |
# File 'lib/menuconform/rules/avail_rules.rb', line 18 def call() contradictions() + declared_never() + indefinite_suspensions() end |
.contradictions(menu) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/menuconform/rules/avail_rules.rb', line 22 def contradictions() out = [] = .doc["availability"] .categories_by_id.each_value do |c| next unless present?(c["availability"]) && present?() next if overlap?(c["availability"], ) out << Finding.new("AVAIL-001", "category", c["id"], "category windows never overlap the menu's windows") end .items_by_id.each_value do |it| next unless present?(it["availability"]) contexts = .categories.select { |c| (c["item_ids"] || []).include?(it["id"]) } .map { |c| present?(c["availability"]) ? c["availability"] : } contexts = [] if contexts.empty? contexts = contexts.select { |w| present?(w) } next if contexts.empty? # nothing to contradict next if contexts.any? { |w| overlap?(it["availability"], w) } out << Finding.new("AVAIL-001", "item", it["id"], "item windows never overlap its category/menu windows — never orderable") end out end |
.declared_never(menu) ⇒ Object
47 48 49 50 51 52 53 54 55 56 |
# File 'lib/menuconform/rules/avail_rules.rb', line 47 def declared_never() out = [] .categories_by_id.each_value do |c| out << Finding.new("AVAIL-002", "category", c["id"], "availability is an explicit empty list") if c["availability"] == [] end .items_by_id.each_value do |it| out << Finding.new("AVAIL-002", "item", it["id"], "availability is an explicit empty list") if it["availability"] == [] end out end |
.indefinite_suspensions(menu) ⇒ Object
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/menuconform/rules/avail_rules.rb', line 58 def indefinite_suspensions() horizon = .reference_time + SUSPENSION_HORIZON_SECONDS check = lambda do |entity, type| raw = entity["suspended_until"] return nil unless raw t = begin Time.iso8601(raw) rescue ArgumentError nil end return nil unless t && t > horizon Finding.new("AVAIL-003", type, entity["id"], "suspended_until #{raw} is more than a year past the reference time — retirement via 86") end .items_by_id.each_value.filter_map { |it| check.call(it, "item") } + .modifiers_by_id.each_value.filter_map { |m| check.call(m, "modifier") } end |
.intervals(windows) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/menuconform/rules/avail_rules.rb', line 86 def intervals(windows) windows.flat_map do |w| days = w["days"] || DAYS s = minute_of_day(w["start"]) e = minute_of_day(w["end"]) days.flat_map do |d| di = DAYS.index(d) next [] if di.nil? || s.nil? || e.nil? base = di * 1440 if e > s [[base + s, base + e]] elsif e < s # wraps past midnight into the next day nxt = ((di + 1) % 7) * 1440 [[base + s, base + 1440], [nxt, nxt + e]].reject { |i| i[0] == i[1] } else [] # zero-length window end end end end |
.minute_of_day(hhmm) ⇒ Object
107 108 109 110 111 |
# File 'lib/menuconform/rules/avail_rules.rb', line 107 def minute_of_day(hhmm) return nil unless hhmm.is_a?(String) && hhmm.match?(/\A\d{2}:\d{2}\z/) h, m = hhmm.split(":").map(&:to_i) (h * 60) + m end |
.overlap?(windows_a, windows_b) ⇒ Boolean
80 81 82 83 84 |
# File 'lib/menuconform/rules/avail_rules.rb', line 80 def overlap?(windows_a, windows_b) ints_a = intervals(windows_a) ints_b = intervals(windows_b) ints_a.any? { |a| ints_b.any? { |b| a[0] < b[1] && b[0] < a[1] } } end |
.present?(windows) ⇒ Boolean
76 77 78 |
# File 'lib/menuconform/rules/avail_rules.rb', line 76 def present?(windows) !windows.nil? && !windows.empty? end |