123 lines
4.0 KiB
Go
123 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
|
|
}
|
|
|
|
|