class Fishing
Constants
- ANALYSIS_DAYS
- BOM_SITES_URL
- BOM_TIDES_TABLE_URL
- CHUNK_DAYS
- LUNAR_CYCLE_DAYS
- LUNAR_REFERENCE_DATE
- MOON_PHASES
- USER_AGENT
Public Class Methods
Source
# File plugins/fishing.rb, line 235 def self.analyse(location, sites:, tide_fetch:, geocode: nil, start_date: Date.today, analysis_days: ANALYSIS_DAYS, chunk_days: CHUNK_DAYS) parsed = parse_location(location) return parsed[:error] if parsed[:error] port = FishingSites.resolve_port(parsed[:location], sites: sites, geocode: geocode) return "Unknown Queensland location: #{parsed[:location]}" unless port tide_days = fetch_tide_days( port, start_date: start_date, tide_fetch: tide_fetch, analysis_days: analysis_days, chunk_days: chunk_days ) return "Could not fetch tide data for #{port[:name]}" if tide_days.empty? ranked = rank_days(tide_days) FishingReport.build_outlook_report( query_location: parsed[:location], port: port, ranked_days: ranked, analysis_days: analysis_days ) rescue StandardError "Could not analyse tides for #{location}" end
Source
# File plugins/fishing.rb, line 78 def self.build_tide_table_url(port, start_date:, days: CHUNK_DAYS) params = { type: "tide", aac: port[:aac], date: format_bom_date(start_date), days: days, region: port[:state_code], offset: 0, offsetName: "", tz: port[:timezone], tz_js: "AEST" } query = params.map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&") "#{BOM_TIDES_TABLE_URL}?#{query}" end
Source
# File plugins/fishing.rb, line 157 def self.daily_run(events) highs = events.select { |event| event[:type] == :high }.map { |event| event[:height] } lows = events.select { |event| event[:type] == :low }.map { |event| event[:height] } return nil if highs.empty? || lows.empty? highs.max - lows.min end
Source
# File plugins/fishing.rb, line 268 def self.dispatch_command(args, sites_fetch: method(:fetch_sites_json), tide_fetch: method(:fetch_tide_html), geocode: nil, start_date: Date.today, **options) text = args.to_s.strip return list_command(sites_fetch: sites_fetch) if text.casecmp?("list") fishing(text, sites_fetch: sites_fetch, tide_fetch: tide_fetch, geocode: geocode, start_date: start_date, **options) end
Source
# File plugins/fishing.rb, line 283 def self.fetch_sites_json RabbotHttp.fetch(BOM_SITES_URL, user_agent: USER_AGENT) end
Source
# File plugins/fishing.rb, line 216 def self.fetch_tide_days(port, start_date:, tide_fetch:, analysis_days:, chunk_days:) days_by_date = {} offset = 0 while offset < analysis_days chunk_start = start_date + offset remaining = analysis_days - offset chunk_size = [chunk_days, remaining].min url = build_tide_table_url(port, start_date: chunk_start, days: chunk_size) html = tide_fetch.call(url) parse_tide_table(html).each do |day| days_by_date[day[:date]] = day end offset += chunk_size end days_by_date.values.sort_by { |day| day[:date] } end
Source
# File plugins/fishing.rb, line 287 def self.fetch_tide_html(url) RabbotHttp.fetch(url, user_agent: USER_AGENT) end
Source
# File plugins/fishing.rb, line 70 def self.find_port(location, sites:) FishingSites.find_port_by_name(location, sites: sites) end
Source
# File plugins/fishing.rb, line 277 def self.fishing(location, sites_fetch: method(:fetch_sites_json), tide_fetch: method(:fetch_tide_html), geocode: nil, start_date: Date.today, **options) sites = queensland_sites_from_json(sites_fetch.call) analyse(location, sites: sites, tide_fetch: tide_fetch, geocode: geocode, start_date: start_date, **options) end
Source
# File plugins/fishing.rb, line 74 def self.format_bom_date(date) format("%02d-%02d-%04d", date.day, date.month, date.year) end
Source
# File plugins/fishing.rb, line 212 def self.format_report_body(_location_name, ranked_days, analysis_days: ANALYSIS_DAYS) FishingReport.format_report_body(ranked_days, analysis_days: analysis_days) end
Source
# File plugins/fishing.rb, line 263 def self.list_command(sites_fetch:) sites = queensland_sites_from_json(sites_fetch.call) FishingReport.build_list_report(FishingListFormat.primary_sites(sites)) end
Source
# File plugins/fishing.rb, line 98 def self.moon_age_days(date) days = (date - LUNAR_REFERENCE_DATE).to_f age = days % LUNAR_CYCLE_DAYS age += LUNAR_CYCLE_DAYS if age.negative? age end
Source
# File plugins/fishing.rb, line 105 def self.moon_phase_for_date(date) age = moon_age_days(date) phase = MOON_PHASES.find { |limit, _name| age < limit }&.last phase || "Waning Crescent" end
Source
# File plugins/fishing.rb, line 165 def self.moon_phase_score(phase) case phase.to_s when /first quarter/i then 3.0 when /last quarter/i then 3.0 when /crescent/i then 2.0 when /gibbous/i then 1.5 when /new moon/i then 1.0 when /full moon/i then 1.0 else 0.0 end end
Source
# File plugins/fishing.rb, line 46 def self.normalize_location(text) FishingSites.normalize_location(text) end
Source
# File plugins/fishing.rb, line 94 def self.parse_height(text) text.to_s[/([\d.]+)/, 1]&.to_f end
Source
# File plugins/fishing.rb, line 59 def self.parse_location(text) location = normalize_location(strip_quotes(text)) return { error: usage_message } if location.empty? { location: location } end
Source
# File plugins/fishing.rb, line 118 def self.parse_tide_day(day_node) moon_img = day_node.at_css(".moon-phase img") moon_phase = moon_img&.[]("title")&.strip events = [] day_node.css("table tbody tr").each do |row| kind = if row.at_css("th.high-tide") :high elsif row.at_css("th.low-tide") :low end next unless kind time_cell = row.at_css("td.localtime") height_cell = row.next_element&.at_css("td.height") next unless time_cell && height_cell height = parse_height(height_cell.text) next unless height time_local = time_cell["data-time-local"] events << { type: kind, height: height, time_local: time_local } end return nil if events.empty? date = Date.parse(events.first[:time_local]) { date: date, moon_phase: resolve_moon_phase(date, moon_phase), events: events } rescue StandardError nil end
Source
# File plugins/fishing.rb, line 150 def self.parse_tide_table(html) return [] if html.to_s.include?("result-error") doc = Nokogiri::HTML(html.to_s) doc.css(".tide-day").filter_map { |day_node| parse_tide_day(day_node) } end
Source
# File plugins/fishing.rb, line 66 def self.queensland_sites_from_json(data) FishingSites.queensland_sites_from_json(data) end
Source
# File plugins/fishing.rb, line 195 def self.rank_days(days) with_run = days.filter_map do |day| run = day[:run] || daily_run(day[:events] || []) next if run.nil? day.merge(run: run) end return [] if with_run.empty? runs = with_run.map { |day| day[:run] } scored = with_run.map do |day| day.merge(score: score_day(day, runs: runs)) end scored.sort_by { |day| [-day[:score], day[:date]] } end
Source
# File plugins/fishing.rb, line 111 def self.resolve_moon_phase(date, provided_phase) phase = provided_phase.to_s.strip return phase unless phase.empty? moon_phase_for_date(date) end
Source
# File plugins/fishing.rb, line 177 def self.run_score(run, runs) return 0.0 if runs.empty? || run.nil? sorted = runs.sort median = sorted[sorted.length / 2] return 10.0 if median.zero? ratio = (run - median).abs / median [10.0 - (ratio * 10.0), 0.0].max end
Source
# File plugins/fishing.rb, line 188 def self.score_day(day, runs:) run = day[:run] moon = moon_phase_score(day[:moon_phase]) tide = run_score(run, runs) (tide * 0.65) + (moon * 0.35) end
Source
# File plugins/fishing.rb, line 50 def self.strip_quotes(text) value = text.to_s.strip if (match = value.match(/\A["'](.+)["']\z/)) match[1].strip else value end end
Source
# File plugins/fishing.rb, line 42 def self.usage_message "Usage: !fishing <location> | list — e.g. !fishing townsville" end
Public Instance Methods
Source
# File plugins/fishing.rb, line 295 def execute(m, location) themed_flood_safe_reply(m, self.class.dispatch_command(location)) end
Source
# File plugins/fishing.rb, line 291 def fishing(location, **options) self.class.fishing(location, **options) end