diff options
Diffstat (limited to 'pkg/adapters/xlsx/model.go')
| -rw-r--r-- | pkg/adapters/xlsx/model.go | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/pkg/adapters/xlsx/model.go b/pkg/adapters/xlsx/model.go new file mode 100644 index 0000000..d8c5194 --- /dev/null +++ b/pkg/adapters/xlsx/model.go @@ -0,0 +1,138 @@ +package xlsx + +import ( + "errors" + "regexp" + "strconv" + "strings" + "time" + + "github.com/leonm1/airports-go" +) + +type Ticket struct { + Sheet string + Passenger string + Title string + FlightNumber string + FromCity string + ToCity string + FromAirport string + ToAirport string + FlightDate string // (raw, expected YYYY-MM-DD; Excel text may start with ') + FlightTime string // (raw, expected HH-MM or HH:MM; Excel text may start with ') + PNR string + Card string + TicketNumber string // (may have a leading ' in Excel) +} + +func (t Ticket) DateTime() (time.Time, *time.Location, error) { + loc := t.inferLocationFromAirports() + date := strings.TrimLeft(strings.TrimSpace(t.FlightDate), "'") + hm := strings.TrimLeft(strings.TrimSpace(t.FlightTime), "'") + hm = strings.ReplaceAll(hm, "-", ":") + + if date == "" || hm == "" { + return time.Time{}, loc, errors.New("missing FlightDate or FlightTime") + } + ts, err := time.ParseInLocation("2006-01-02 15:04", date+" "+hm, loc) + return ts, loc, err +} + +func (t Ticket) inferLocationFromAirports() *time.Location { + if loc := iataToLocation(t.FromAirport); loc != nil { + return loc + } + if loc := iataToLocation(t.ToAirport); loc != nil { + return loc + } + return time.Local +} + +func iataToLocation(code string) *time.Location { + iata := strings.ToUpper(strings.TrimSpace(code)) + if len(iata) != 3 { + return nil + } + ap, err := airports.LookupIATA(iata) + if err != nil { + return nil + } + // Prefer IANA tz name + if tz := strings.TrimSpace(ap.Tz); tz != "" && tz != `\N` { + if loc, err := time.LoadLocation(tz); err == nil { + return loc + } + } + // Fallback: fixed offset (no DST) + if ap.Timezone != 0 { + sec := int(ap.Timezone * 3600.0) + return time.FixedZone("UTC"+offsetLabel(sec), sec) + } + return nil +} + +func offsetLabel(sec int) string { + sign := "+" + if sec < 0 { + sign = "-" + sec = -sec + } + h := sec / 3600 + m := (sec % 3600) / 60 + return sign + two(h) + ":" + two(m) +} +func two(x int) string { + if x < 10 { + return "0" + strconv.Itoa(x) + } + return strconv.Itoa(x) +} + +func parseCardLine(s string) (prefix string, number uint64, bonus string) { + raw := strings.TrimSpace(s) + if raw == "" { + return "", 0, "" + } + // number = last run of digits + if m := regexp.MustCompile(`(\d{3,})\D*$`).FindStringSubmatch(raw); len(m) == 2 { + if n, err := strconv.ParseUint(m[1], 10, 64); err == nil { + number = n + } + } + + // tokens (letters with '-', '/', apostrophes) + tokRe := regexp.MustCompile(`[A-Za-z][A-Za-z'/-]*`) + toks := tokRe.FindAllString(s, -1) + + // prefix = first 2–3 letter all-caps-ish token + for _, t := range toks { + u := strings.ToUpper(t) + if len(u) >= 2 && len(u) <= 3 && regexp.MustCompile(`^[A-Z]{2,3}$`).MatchString(u) { + prefix = u + break + } + } + // bonus = all tokens except prefix + words := []string{} + for _, t := range toks { + if strings.ToUpper(t) == prefix { + continue + } + words = append(words, t) + } + if len(words) > 0 { + bonus = strings.Join(words, " ") + } + if bonus == "" && prefix != "" { + bonus = prefix + } + return +} + +func firstNonEmpty(a, b string) string { + if strings.TrimSpace(a) != "" { + return a + } + return b +} |
