inou/.claude/worktrees/vibrant-nash/lib/lab_reference.go

122 lines
4.0 KiB
Go

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
}