require "json" require "./data_files" module JosieHealth::LUT module MedsLUT @@medications : Hash(String, NamedTuple( name: String, pretty_name: String, default_concentration: Int32, category: String, subcategory: String))? @@alias_map : Hash(String, String)? @@loaded = false # Load medication data from embedded JSON def self.load_data return if @@loaded begin json = DataFiles.meds @@medications = {} of String => NamedTuple(name: String, pretty_name: String, default_concentration: Int32, category: String, subcategory: String) @@alias_map = {} of String => String if meds_json = json["medications"]? meds_json.as_h.each do |name, data| canonical_name = name.to_s @@medications.not_nil![canonical_name] = { name: data["name"]?.try(&.as_s) || canonical_name, pretty_name: data["pretty_name"]?.try(&.as_s) || canonical_name, default_concentration: data["default_concentration_mg_ml"].as_i, category: data["category"]?.try(&.as_s) || "medication", subcategory: data["subcategory"]?.try(&.as_s) || "", } # Register aliases 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 puts "Loaded #{@@medications.not_nil!.size} medications with #{@@alias_map.not_nil!.size} aliases" @@loaded = true rescue ex puts "Error loading meds.json: #{ex.message}" @@medications = {} of String => NamedTuple(name: String, pretty_name: String, default_concentration: Int32, category: String, subcategory: String) @@alias_map = {} of String => String @@loaded = true end end # Resolve alias to canonical name def self.resolve_alias(name : String) : String load_data normalized = name.downcase.gsub(/[\s\-_]/, "") @@alias_map.not_nil![normalized]? || normalized end # Lookup a medication by name (handles aliases) def self.lookup_medication(name : String) : NamedTuple(name: String, pretty_name: String, default_concentration: Int32, category: String, subcategory: String)? load_data # Handle @concentration suffix base_name = name.split("@").first canonical = resolve_alias(base_name) @@medications.not_nil![canonical]? end # Check if a substance is an injectable medication def self.is_injectable_med?(name : String) : Bool lookup_medication(name) != nil end # Parse substance name with optional concentration override # Returns {substance, concentration} or nil # Examples: "een" -> {een, nil}, "een@20" -> {een, 20} def self.parse_substance_concentration(name : String) : NamedTuple(substance: String, concentration: Int32?)? parts = name.split("@", 2) substance = parts[0] return nil unless is_injectable_med?(substance) concentration = if parts.size > 1 && !parts[1].empty? parts[1].to_i? else nil end {substance: substance, concentration: concentration} end # Calculate mg from ml and concentration def self.calculate_mg(volume_ml : Float64, concentration_mg_ml : Int32) : Float64 (volume_ml * concentration_mg_ml).round(1) end # Full medication dose calculation # Input: substance name (possibly with @concentration), dosage string, unit # Returns: {mg, canonical_name, annotation} or nil def self.calculate_dose(substance : String, dosage : String, unit : String) : NamedTuple(mg: Float64, canonical_name: String, annotation: String)? # Only process if unit is ml return nil unless unit.downcase == "ml" parsed = parse_substance_concentration(substance) return nil unless parsed med_info = lookup_medication(parsed[:substance]) return nil unless med_info # Parse dosage as float volume_ml = dosage.to_f? return nil unless volume_ml # Use override concentration or default concentration = parsed[:concentration] || med_info[:default_concentration] mg = calculate_mg(volume_ml, concentration) details = "#{volume_ml}ml @ #{concentration}mg/ml" {mg: mg, canonical_name: med_info[:name], annotation: details} end # Get all medication names def self.all_medications : Array(String) load_data @@medications.not_nil!.keys end end end