aboutsummaryrefslogtreecommitdiff
path: root/pkg/adapters/xlsx/model.go
blob: d8c5194029d407db053bfe14beb4ffbfd2263d11 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
}