yoake/internal/health/parser.go

244 lines
6.3 KiB
Go

package health
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/eternal-flame-AD/yoake/internal/util"
)
type Direction struct {
Name string `json:"name"`
PeriodHours int `json:"period_hours"`
Dosage int `json:"dosage"`
DosageUnit string `json:"dosage_unit"`
DosageRoute string `json:"dosage_route"`
Flags []DirectionFlag `json:"flags"`
DirectionShorthand string `json:"shorthand"`
OptSchedule OptSchedule `json:"schedule"`
Disclaimer string `json:"__disclaimer"`
}
const DirectionDisclaimer = "For personal use only. No warranty of accuracy."
type DirectionFlag string
type OptSchedule string
const (
DirectionFlagAM DirectionFlag = "qam"
DirectionFlagHS DirectionFlag = "qhs"
DirectionFlagPRN DirectionFlag = "prn"
DirectionFlagAdLib DirectionFlag = "ad lib"
OptScheduleDefault OptSchedule = "default"
OptScheduleWholeDose OptSchedule = "whole"
)
func ParseShorthand(shorthand string) (*Direction, error) {
res := new(Direction)
res.Disclaimer = DirectionDisclaimer
res.Flags = make([]DirectionFlag, 0)
words := strings.Split(shorthand, " ")
optionsRegex := regexp.MustCompile(`^([a-zA-Z]+)\((\w+)\)$`)
for i := len(words) - 1; i >= 0; i-- {
if match := optionsRegex.FindStringSubmatch(words[i]); match != nil {
name, value := match[1], match[2]
switch strings.ToLower(name) {
case "sched":
fallthrough
case "schedule":
if res.OptSchedule != "" {
return nil, fmt.Errorf("duplicate schedule option")
}
switch strings.ToLower(value) {
case "default":
fallthrough
case "":
res.OptSchedule = OptScheduleDefault
case "whole":
res.OptSchedule = OptScheduleWholeDose
default:
return nil, fmt.Errorf("invalid schedule option: %s", value)
}
default:
return nil, fmt.Errorf("unknown option %s", name)
}
words = words[:i]
}
}
if res.OptSchedule == "" {
res.OptSchedule = OptScheduleDefault
}
// combined numbers and units
for i := range words {
digits := regexp.MustCompile(`^\d+$`)
if digits.MatchString(words[i]) {
words[i] = words[i] + words[i+1]
words[i+1] = ""
}
if strings.ToLower(words[i]) == "ad" && strings.ToLower(words[i+1]) == "lib" {
res.Flags = append(res.Flags, DirectionFlagAdLib)
words[i] = ""
words[i+1] = ""
} else if strings.ToLower(words[i]) == "adlib" {
res.Flags = append(res.Flags, DirectionFlagAdLib)
words[i] = ""
}
}
words = util.AntiJoin(words, []string{""})
// find prn keyword
for i := len(words) - 1; i >= 0; i-- {
if strings.EqualFold(words[i], "prn") {
if util.Contain(res.Flags, DirectionFlagAdLib) {
return nil, fmt.Errorf("cannot use 'ad lib' and 'prn' together")
}
res.Flags = append(res.Flags, DirectionFlagPRN)
words = append(words[:i], words[i+1:]...)
break
}
}
freqIdx := len(words) - 1
if lastWord := strings.ToLower(words[len(words)-1]); lastWord == "bid" {
res.PeriodHours = 12
words = words[:len(words)-1]
} else if lastWord == "tid" {
res.PeriodHours = 8
words = words[:len(words)-1]
} else if lastWord == "qid" {
res.PeriodHours = 6
words = words[:len(words)-1]
} else {
for i := len(words) - 1; i >= 0; i-- {
if strings.HasPrefix(strings.ToLower(words[i]), "q") {
freqIdx = i
break
}
}
freqStr := strings.ToLower(strings.Join(words[freqIdx:], ""))[1:]
if freqStr == "am" {
res.Flags = append(res.Flags, DirectionFlagAM)
res.PeriodHours = 24
} else if freqStr == "hs" {
res.Flags = append(res.Flags, DirectionFlagHS)
res.PeriodHours = 24
} else {
if !(freqStr[0] >= '0' && freqStr[0] <= '9') {
freqStr = "1" + freqStr
}
freqRegexp := regexp.MustCompile(`^([0-9]+)([a-z]+)$`)
freqMatch := freqRegexp.FindStringSubmatch(freqStr)
if freqMatch == nil {
return nil, fmt.Errorf("invalid frequency: %s", freqStr)
}
freq, err := strconv.Atoi(freqMatch[1])
if err != nil {
return nil, fmt.Errorf("invalid frequency number : %s", freqMatch[1])
}
if freqMatch[2] == "d" {
res.PeriodHours = freq * 24
} else if freqMatch[2] == "h" {
res.PeriodHours = freq
} else {
return nil, fmt.Errorf("invalid frequency unit: %s", freqMatch[2])
}
}
}
words = words[:freqIdx]
dosageRegexp := regexp.MustCompile(`^([0-9]+)([a-z]*)$`)
var dosageMatch []string
if dosageMatch = dosageRegexp.FindStringSubmatch(words[len(words)-1]); dosageMatch == nil {
if dosageMatch = dosageRegexp.FindStringSubmatch(words[len(words)-2]); dosageMatch == nil {
return nil, fmt.Errorf("invalid dosage: %s", words[len(words)-2:])
} else {
res.DosageRoute = words[len(words)-1]
words = words[:len(words)-2]
}
} else {
words = words[:len(words)-1]
}
dosage, err := strconv.Atoi(dosageMatch[1])
if err != nil {
return nil, fmt.Errorf("invalid dosage number: %s", dosageMatch[1])
}
res.Dosage = dosage
res.DosageUnit = dosageMatch[2]
res.Name = strings.Join(words, " ")
s1, s2 := res.ShortHand()
res.DirectionShorthand = s1 + " " + s2
return res, nil
}
func (d Direction) KeyName() string {
return strings.ToLower(strings.SplitN(d.Name, " ", 2)[0])
}
func (d *Direction) ShortHand() (name string, direction string) {
builder := new(strings.Builder)
builder.WriteString(strconv.Itoa(d.Dosage))
if d.DosageUnit != "" {
builder.WriteString(" ")
builder.WriteString(d.DosageUnit)
}
if d.DosageRoute != "" {
builder.WriteString(" ")
builder.WriteString(d.DosageRoute)
}
if d.PeriodHours%24 == 0 {
qNd := d.PeriodHours / 24
qNdS := strconv.Itoa(qNd)
if qNd == 1 {
if util.Contain(d.Flags, DirectionFlagAM) {
qNdS = "AM"
} else if util.Contain(d.Flags, DirectionFlagHS) {
qNdS = "HS"
} else {
qNdS = "d"
}
} else {
qNdS += "d"
}
fmt.Fprintf(builder, " q%s", qNdS)
} else if d.PeriodHours == 12 {
builder.WriteString(" bid")
} else if d.PeriodHours == 8 {
builder.WriteString(" tid")
} else if d.PeriodHours == 6 {
builder.WriteString(" qid")
} else {
fmt.Fprintf(builder, " q%sh", strconv.Itoa(d.PeriodHours))
}
if util.Contain(d.Flags, DirectionFlagPRN) {
builder.WriteString(" PRN")
} else if util.Contain(d.Flags, DirectionFlagAdLib) {
builder.WriteString(" ad lib")
}
if d.OptSchedule != "" && d.OptSchedule != OptScheduleDefault {
if d.OptSchedule == OptScheduleWholeDose {
builder.WriteString(" sched(whole)")
}
}
return d.Name, builder.String()
}
type Dose struct {
Time time.Time
Dose int
}