module ClockFlipStore
ClockFlipStore — persist upside-down clock flip meanings (manual !time entries and cached lookups). Public: parse_time_label, load_entry, save_entry, save_manual, valid_flip_word? Depends: ClockFlip, RabbotDb Tests: test/lib/clock_flip_store_test.rb
Constants
- KEY_PREFIX
- PLUGIN
- TIME_PATTERN
Public Instance Methods
Source
# File lib/clock/flip_store.rb, line 35 def cache_key(hour, minute) "#{KEY_PREFIX}#{format('%02d:%02d', hour.to_i, minute.to_i)}" end
Source
# File lib/clock/flip_store.rb, line 39 def expected_flip_word(hour, minute) ClockFlip.upside_down_word(hour, minute) end
Source
# File lib/clock/flip_store.rb, line 51 def load_entry(hour:, minute:, store: RabbotDb) raw = store.get_plugin_json(plugin: PLUGIN, key: cache_key(hour, minute), default: nil) normalize_entry(raw) end
Source
# File lib/clock/flip_store.rb, line 56 def load_entry_by_key(key, store: RabbotDb) raw = store.get_plugin_json(plugin: PLUGIN, key: key.to_s, default: nil) normalize_entry(raw) end
Source
# File lib/clock/flip_store.rb, line 134 def manual_entry?(entry) entry.is_a?(Hash) && entry["source"].to_s == "manual" end
Source
# File lib/clock/flip_store.rb, line 85 def merge_unique_lines(existing, incoming) seen = {} Array(existing).concat(Array(incoming)).filter_map do |line| text = line.to_s.strip next if text.empty? key = text.downcase next if seen[key] seen[key] = true text end end
Source
# File lib/clock/flip_store.rb, line 138 def normalize_entry(raw) return nil unless raw.is_a?(Hash) lines = Array(raw["lines"]).map(&:to_s).reject { |line| line.strip.empty? } return nil if lines.empty? { "flip_word" => raw["flip_word"].to_s.strip.downcase, "lines" => lines, "source" => raw["source"].to_s, "cached_at" => raw["cached_at"].to_s, "last_shown" => raw["last_shown"].to_s }.tap { |entry| entry.delete("last_shown") if entry["last_shown"].empty? } end
Source
# File lib/clock/flip_store.rb, line 19 def parse_time_label(text) match = text.to_s.strip.match(TIME_PATTERN) return nil unless match hour = match[1].to_i minute = match[2].to_i return nil unless hour.between?(0, 23) && minute.between?(0, 59) { hour: hour, minute: minute, key: cache_key(hour, minute), time_label: ClockFlip.format_time(hour, minute) } end
Source
# File lib/clock/flip_store.rb, line 76 def record_last_shown!(hour:, minute:, line:, store: RabbotDb) entry = load_entry(hour: hour, minute: minute, store: store) return nil unless entry entry = entry.merge("last_shown" => line.to_s) store.set_plugin_json(plugin: PLUGIN, key: cache_key(hour, minute), value: entry) entry end
Source
# File lib/clock/flip_store.rb, line 61 def save_entry(hour:, minute:, flip_word:, lines:, source:, store: RabbotDb, now: Time.now) existing = load_entry(hour: hour, minute: minute, store: store) merged_lines = merge_unique_lines(existing&.fetch("lines", nil), lines) entry = { "flip_word" => flip_word.to_s.strip.downcase, "lines" => merged_lines, "source" => source.to_s, "cached_at" => RabbotDb.iso_time(now) } entry["last_shown"] = existing["last_shown"] if existing&.key?("last_shown") store.set_plugin_json(plugin: PLUGIN, key: cache_key(hour, minute), value: entry) entry end
Source
# File lib/clock/flip_store.rb, line 99 def save_manual(hour:, minute:, abbrev:, meaning:, store: RabbotDb) parsed = parse_time_label(ClockFlip.format_time(hour, minute)) return { error: "Invalid time." } unless parsed expected = expected_flip_word(hour, minute) token = abbrev.to_s.strip.downcase unless valid_flip_word?(hour, minute, token) options = valid_flip_words(hour, minute) option_text = options.empty? ? expected : options.join(", ") return { error: "Abbreviation must be one of: #{option_text} for #{parsed[:time_label]} (got #{token.empty? ? '?' : token})." } end meaning_text = meaning.to_s.strip return { error: "Meaning is required." } if meaning_text.empty? line = "#{token} — #{meaning_text}" existing = load_entry(hour: hour, minute: minute, store: store) merged_lines = if existing merge_unique_lines(existing["lines"], [line]) else [line] end entry = save_entry( hour: hour, minute: minute, flip_word: token, lines: merged_lines, source: "manual", store: store ) { entry: entry, time_label: parsed[:time_label] } end
Source
# File lib/clock/flip_store.rb, line 47 def valid_flip_word?(hour, minute, word) ClockFlip.valid_flip_word?(hour, minute, word) end
Source
# File lib/clock/flip_store.rb, line 43 def valid_flip_words(hour, minute) ClockFlip.upside_down_candidates(hour, minute) end