require "json" require "./data_files" module JosieHealth::LUT module CannabisLUT @@sources : Hash(String, NamedTuple(default_amount: Float64, unit: String, category: String))? @@alias_map : Hash(String, String)? @@serving_units : Hash(String, Int32)? @@loaded = false def self.load_data return if @@loaded begin json = DataFiles.cannabis @@sources = {} of String => NamedTuple(default_amount: Float64, unit: String, category: String) @@alias_map = {} of String => String @@serving_units = {} of String => Int32 if sources_json = json["sources"]? sources_json.as_h.each do |name, data| canonical_name = name.to_s unit = data["unit"].as_s default_amount = if unit == "g" data["default_g"].as_f else data["default_mg"].as_f end @@sources.not_nil![canonical_name] = { default_amount: default_amount, unit: unit, category: data["category"].as_s, } if aliases = data["aliases"]? aliases.as_a.each do |alias_name| @@alias_map.not_nil![alias_name.as_s.downcase] = canonical_name end end end end if serving_json = json["serving_units"]? serving_json.as_h.each do |name, multiplier| @@serving_units.not_nil![name.to_s] = multiplier.as_i end end puts "Loaded #{@@sources.not_nil!.size} cannabis sources with #{@@alias_map.not_nil!.size} aliases" @@loaded = true rescue ex puts "Error loading cannabis.json: #{ex.message}" @@sources = {} of String => NamedTuple(default_amount: Float64, unit: String, category: String) @@alias_map = {} of String => String @@serving_units = {} of String => Int32 @@loaded = true end end def self.resolve_alias(name : String) : String load_data normalized = name.downcase.gsub(/[\s\-_]/, "") @@alias_map.not_nil![normalized]? || normalized end def self.lookup_source(name : String) : NamedTuple(default_amount: Float64, unit: String, category: String)? load_data canonical = resolve_alias(name) @@sources.not_nil![canonical]? end def self.is_cannabis_source?(name : String) : Bool lookup_source(name) != nil end def self.parse_dosage(dosage : String) : NamedTuple(count: Float64, serving: String?)? normalized = dosage.downcase.strip load_data if match = normalized.match(/^(\d+(?:\/\d+)?(?:\.\d+)?)(puff|puffs|hit|hits|rip|rips|toke|tokes)?$/) count_str = match[1] serving = match[2]? count = if count_str.includes?("/") parts = count_str.split("/") parts[0].to_f / parts[1].to_f else count_str.to_f end return {count: count, serving: serving} end nil end def self.calculate_dose(dosage : String, source : String) : NamedTuple(amount: Float64, unit: String, details: String)? parsed = parse_dosage(dosage) return nil unless parsed source_info = lookup_source(source) return nil unless source_info count = parsed[:count] if parsed[:serving] count = count * (@@serving_units.not_nil![parsed[:serving]]? || 1) end amount = (source_info[:default_amount] * count).round(2) canonical = resolve_alias(source) count_str = if parsed[:count] == parsed[:count].to_i parsed[:count].to_i.to_s else parsed[:count].round(2).to_s end serving_str = parsed[:serving] ? "#{count_str}#{parsed[:serving]}" : count_str details = "#{canonical} #{serving_str} (#{amount}#{source_info[:unit]})" {amount: amount, unit: source_info[:unit], details: details} end def self.all_sources : Array(String) load_data @@sources.not_nil!.keys end end end