ot", "cocktail" => nil}
CATEGORY_DEFAULTS = {"beer" => {abv: 5.0, default_ml: 330}, "cider" => {abv: 5.0, default_ml: 440}, "wine" => {abv: 12.5, default_ml: 150}, "spirits" => {abv: 40.0, default_ml: 50}, "liqueur" => {abv: 25.0, default_ml: 50}, "cocktail" => {abv: 12.0, default_ml: 200}, "seltzer" => {abv: 5.0, default_ml: 355}}

Category defaults

ETHANOL_DENSITY = 0.789
SERVING_SIZES = {"shot" => 50, "shots" => 50, "bottle" => 330, "bottles" => 330, "pint" => 568, "pints" => 568, "glass" => 150, "glasses" => 150, "can" => 330, "cans" => 330, "unit" => 1, "units" => 1}

Serving sizes in ml

Class Method Summary

Class Method Detail

def self.all_brands : Array(String) #

Get all brand names


def self.calculate_dose(dosage : String, substance : String, abv_override : Float64 | Nil = nil) : NamedTuple(grams: Float64, annotation: String) | Nil #

Full alcohol dose calculation Input: dosage string, substance name, optional ABV override Returns: {grams, annotation} or nil


def self.calculate_grams(volume_ml : Float64, abv_percent : Float64) : Float64 #

Calculate alcohol grams from volume and ABV


def self.default_serving_for(name : String) : String | Nil #

Get the default serving unit for a brand based on its category


def self.get_category_defaults(category : String) : NamedTuple(abv: Float64, default_ml: Int32) | Nil #

Get category defaults


def self.is_alcohol?(name : String) : Bool #

Check if a substance is an alcoholic beverage


def self.load_data #

Load alcohol data from embedded JSON


def self.lookup_brand(name : String) : NamedTuple(abv: Float64, category: String, style: String, default_ml: Int32) | Nil #

Lookup a brand by name


def self.parse_abv(abv_str : String) : Float64 | Nil #

Parse ABV from string like "5%", "5.5%", "40"


def self.parse_dosage(dosage : String) : NamedTuple(count: Int32, serving: String | Nil, volume_ml: Int32) | Nil #

Parse dosage string like "2pints", "500ml", "1bottle" Returns {count, serving_type, volume_ml} or nil if not parseable