package lib import ( "fmt" "math" ) // LabScale is the multiplier for storing float values as int64 (6 decimal places) const LabScale = 1000000 // LabNoRef sentinel: ref_low or ref_high not set (no bound) const LabNoRef = int64(-1) // ToLabScale converts float64 to scaled int64 func ToLabScale(f float64) int64 { return int64(math.Round(f * LabScale)) } // FromLabScale converts scaled int64 back to float64 func FromLabScale(i int64) float64 { return float64(i) / LabScale } // Direction constants for LabTest const ( DirRange = "range" // both out-of-range bad (default) DirLowerBetter = "lower_better" // low = good (CRP, LDL, glucose) DirHigherBetter = "higher_better" // high = good (HDL, hemoglobin) ) // LabTest holds per-LOINC-code test properties. // Populated lazily: when we encounter a new test during normalization, // we ask the LLM to provide LOINC + SI unit + direction. type LabTest struct { LoincID string `db:"loinc_id,pk"` // LOINC code e.g. "718-7" Name string `db:"name"` // English canonical e.g. "Hemoglobin" SIUnit string `db:"si_unit"` // SI unit e.g. "g/L" Direction string `db:"direction"` // "range", "lower_better", "higher_better" SIFactor int64 `db:"si_factor"` // conventional→SI multiplier x1M (e.g. 10.0 = 10000000) } // LabReference holds reference ranges, always in SI units. // Composite key emulated via synthetic ref_id. type LabReference struct { RefID string `db:"ref_id,pk"` // "loinc|source|sex|ageDays" LoincID string `db:"loinc_id"` // FK to lab_test Source string `db:"source"` // "CALIPER", "IFCC" Sex string `db:"sex"` // "", "M", "F" AgeDays int64 `db:"age_days"` // start of age range (0 = birth) AgeEnd int64 `db:"age_end"` // end of age range in days RefLow int64 `db:"ref_low"` // lower bound x1M (-1 = no bound) RefHigh int64 `db:"ref_high"` // upper bound x1M (-1 = no bound) Unit string `db:"unit"` // SI unit these values are in } // MakeRefID builds synthetic PK for LabReference func MakeRefID(loinc, source, sex string, ageDays int64) string { return fmt.Sprintf("%s|%s|%s|%d", loinc, source, sex, ageDays) } // LabTestGet retrieves a LabTest by LOINC code. Returns nil if not found. func LabTestGet(loincID string) (*LabTest, error) { var tests []LabTest if err := RefQuery("SELECT * FROM lab_test WHERE loinc_id = ?", []any{loincID}, &tests); err != nil || len(tests) == 0 { return nil, err } return &tests[0], nil } // LabRefLookupAll returns all reference ranges for a LOINC code. func LabRefLookupAll(loincID string) ([]LabReference, error) { var refs []LabReference return refs, RefQuery("SELECT ref_id, loinc_id, source, sex, age_days, age_end, ref_low, ref_high, unit FROM lab_reference WHERE loinc_id = ?", []any{loincID}, &refs) } // LabRefLookup finds the matching reference range for a test at a given age/sex. // Returns nil if no matching reference found. func LabRefLookup(loincID, sex string, ageDays int64) (*LabReference, error) { var refs []LabReference if err := RefQuery( "SELECT ref_id, loinc_id, source, sex, age_days, age_end, ref_low, ref_high, unit FROM lab_reference WHERE loinc_id = ?", []any{loincID}, &refs, ); err != nil { return nil, err } // Find best match: exact sex > unisex, narrowest age range var best *LabReference bestScore := -1 for i := range refs { r := &refs[i] // Age must be in range if ageDays < r.AgeDays || ageDays > r.AgeEnd { continue } score := 0 // Prefer sex-specific over unisex if r.Sex == sex { score += 10 } else if r.Sex != "" { continue // wrong sex } // Prefer narrower age range span := r.AgeEnd - r.AgeDays if span < 365*100 { score += int(365*100 - span) // narrower = higher score } if score > bestScore { best = r bestScore = score } } return best, nil } // AgeDays calculates age in days from DOB unix timestamp to a given timestamp. func AgeDays(dobUnix, atUnix int64) int64 { return (atUnix - dobUnix) / 86400 }