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 }