aboutsummaryrefslogtreecommitdiff
path: root/pkg/adapters/xlsx/model.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/adapters/xlsx/model.go')
-rw-r--r--pkg/adapters/xlsx/model.go138
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
+}