inou/lib/normalize.go

150 lines
3.7 KiB
Go

package lib
import (
"encoding/json"
"fmt"
"log"
"strings"
)
// Normalize resolves hospital test names to official LOINC codes and updates entries.
// Flow: hospital name + specimen + unit → LOINC (via cache or Gemini lookup) → official name from loinc_lab.
// No Fireworks LLM. Original Type field is never modified.
func Normalize(dossierID string, category int, progress ...func(processed, total int)) error {
reportProgress := func(p, t int) {
if len(progress) > 0 && progress[0] != nil {
progress[0](p, t)
}
}
// 1. Load all entries, build parent map for specimen lookup
entries, err := EntryQueryOld(dossierID, category, "")
if err != nil {
return fmt.Errorf("load entries: %w", err)
}
parentMap := make(map[string]*Entry)
for _, e := range entries {
if e.ParentID == "" || e.Type == "lab_order" {
parentMap[e.EntryID] = e
}
}
// 2. Collect unique type|specimen|unit combos, resolve each to LOINC
type testKey struct {
name, specimen, unit string
}
type resolved struct {
loinc string
info *LoincInfo
abbr string
}
cache := make(map[testKey]*resolved)
var lookupCount, cacheHits, misses int
for _, e := range entries {
if e.ParentID == "" || e.Type == "lab_order" || e.Type == "" {
continue
}
var data map[string]interface{}
json.Unmarshal([]byte(e.Data), &data)
unit, _ := data["unit"].(string)
specimen := ""
if parent, ok := parentMap[e.ParentID]; ok {
var pdata map[string]interface{}
if json.Unmarshal([]byte(parent.Data), &pdata) == nil {
specimen, _ = pdata["specimen"].(string)
}
}
tk := testKey{e.Type, specimen, unit}
if _, ok := cache[tk]; ok {
continue
}
lookupCount++
loinc := LoincLookup(e.Type, specimen, unit)
if loinc == "" {
cache[tk] = &resolved{}
misses++
continue
}
info := LoincGet(loinc)
abbr := LoincAbbr(info)
cache[tk] = &resolved{loinc: loinc, info: info, abbr: abbr}
if info != nil {
cacheHits++
}
}
reportProgress(lookupCount, lookupCount)
log.Printf("normalize: %d unique combos, %d resolved, %d unresolved", lookupCount, cacheHits, misses)
// 3. Apply to entries
var toSave []Entry
for _, e := range entries {
if e.ParentID == "" || e.Type == "lab_order" || e.Type == "" {
continue
}
var data map[string]interface{}
if json.Unmarshal([]byte(e.Data), &data) != nil {
data = make(map[string]interface{})
}
unit, _ := data["unit"].(string)
specimen := ""
if parent, ok := parentMap[e.ParentID]; ok {
var pdata map[string]interface{}
if json.Unmarshal([]byte(parent.Data), &pdata) == nil {
specimen, _ = pdata["specimen"].(string)
}
}
r := cache[testKey{e.Type, specimen, unit}]
if r == nil || r.loinc == "" || r.info == nil {
continue
}
// Check if already up to date
existingLoinc, _ := data["loinc"].(string)
existingName, _ := data["normalized_name"].(string)
if existingLoinc == r.loinc && existingName == r.info.LongName && e.SearchKey == r.loinc && e.SearchKey2 != "" {
continue
}
// Update Data JSON
data["loinc"] = r.loinc
data["normalized_name"] = r.info.LongName
data["abbreviation"] = r.abbr
b, _ := json.Marshal(data)
e.Data = string(b)
// Update search keys
e.SearchKey = r.loinc
e.SearchKey2 = strings.ToLower(r.info.LongName)
// Rebuild Summary: "Abbr: value unit"
summary := r.abbr + ": " + e.Value
if unit != "" {
summary += " " + unit
}
e.Summary = summary
toSave = append(toSave, *e)
}
if len(toSave) == 0 {
log.Printf("normalize: no changes needed")
return nil
}
log.Printf("normalize: updating %d entries", len(toSave))
ptrs := make([]*Entry, len(toSave))
for i := range toSave {
ptrs[i] = &toSave[i]
}
return EntryWrite("", ptrs...)
}