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
}
|