From bb833561aa74f02970aee13cdc75973b29716491 Mon Sep 17 00:00:00 2001 From: leshe4ka46 Date: Mon, 27 Oct 2025 20:36:28 +0300 Subject: # This is a combination of 2 commits. # This is the 1st commit message: unmarshal all formats, merge them in the single table, users are truly unique # This is the commit message #2: i --- pkg/localstore/store.go | 638 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 638 insertions(+) create mode 100644 pkg/localstore/store.go (limited to 'pkg/localstore/store.go') diff --git a/pkg/localstore/store.go b/pkg/localstore/store.go new file mode 100644 index 0000000..151b2b4 --- /dev/null +++ b/pkg/localstore/store.go @@ -0,0 +1,638 @@ +package localstore + +import ( + "errors" + "strings" + "sync" + "time" + "unicode/utf8" + + "airlines/pkg/model" +) + +type userNameKey struct { + Surname string + Name string + Fathersname string // "" allowed + BirthYMD int32 // 0 if unknown +} + +type userInitKey struct { + Surname string + Name string + Init string // one letter + BirthYMD int32 +} + +type cardKey struct { + Prefix string + Bonus string // may be "" + Number uint64 +} + +type cardPairKey struct { + Prefix string + Number uint64 +} + +type flightKey struct { + Number string + From string + To string + DateYMD int32 + HasTime bool + Sec int32 // seconds since local midnight if HasTime +} + +type flightDayKey struct { + Number string + From string + To string + DateYMD int32 +} + + +type Store struct { + mu sync.RWMutex + + users []*model.User + cards []*model.Card + flights []*model.Flight + + nickToUID map[string]uint64 + nameToUID map[userNameKey]uint64 + nameInitToUID map[userInitKey]uint64 + + cardToCID map[cardKey]uint64 + cardPairToCID map[cardPairKey]uint64 + + flightToFID map[flightKey]uint64 + flightByDay map[flightDayKey]uint64 + + userFlights map[uint64]map[uint64]struct{} // user_id -> set(flight_id) + cardFlights map[uint64]map[uint64]struct{} // card_id -> set(flight_id) + + codesByUser map[uint64]map[string]struct{} // user_id -> set(code) + countriesByUser map[uint64]map[string]struct{} // user_id -> set(country) + cardsByUser map[uint64]map[uint64]struct{} // user_id -> set(card_id) +} + +func NewLocalStore() *Store { + return &Store{ + users: make([]*model.User, 1), + cards: make([]*model.Card, 1), + flights: make([]*model.Flight, 1), + nickToUID: make(map[string]uint64, 1<<12), + nameToUID: make(map[userNameKey]uint64, 1<<12), + cardToCID: make(map[cardKey]uint64, 1<<14), + cardPairToCID: make(map[cardPairKey]uint64, 1<<14), + flightToFID: make(map[flightKey]uint64, 1<<15), + flightByDay: make(map[flightDayKey]uint64, 1<<15), + userFlights: make(map[uint64]map[uint64]struct{}, 1<<12), + cardFlights: make(map[uint64]map[uint64]struct{}, 1<<12), + nameInitToUID: make(map[userInitKey]uint64, 1<<12), + codesByUser: make(map[uint64]map[string]struct{}, 1<<12), + countriesByUser: make(map[uint64]map[string]struct{}, 1<<12), + cardsByUser: make(map[uint64]map[uint64]struct{}, 1<<12), + } +} + + +func isZeroCoord(c model.LatLong) bool { return c.Lat == 0 && c.Long == 0 } + +func ymdUTC(t time.Time) int32 { + if t.IsZero() || t.Year() == model.SentinelBirthday().Year() { + return 0 + } + u := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC) + return int32(u.Year()*10000 + int(u.Month())*100 + u.Day()) +} + +func secSinceMidnight(t time.Time) int32 { + h, m, s := t.Clock() + return int32(h*3600 + m*60 + s) +} + +func ensureSet(m map[uint64]map[uint64]struct{}, k uint64) map[uint64]struct{} { + s, ok := m[k] + if !ok { + s = make(map[uint64]struct{}, 8) + m[k] = s + } + return s +} + +/* ============================== users ============================= */ + +func fatherInitial(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + r, _ := utf8.DecodeRuneInString(s) + if r == utf8.RuneError { + return "" + } + return string(r) // your pipeline keeps it UPPER +} + +func addUserInitIndex(m map[userInitKey]uint64, u *model.User) { + k := userInitKey{Surname: u.Surname, Name: u.Name, Init: fatherInitial(u.Fathersname), BirthYMD: ymdUTC(u.Birthday)} + if _, exists := m[k]; !exists { + m[k] = u.ID + } +} + +func delUserInitIndex(m map[userInitKey]uint64, u *model.User) { + k := userInitKey{Surname: u.Surname, Name: u.Name, Init: fatherInitial(u.Fathersname), BirthYMD: ymdUTC(u.Birthday)} + delete(m, k) +} + +// --- merge helper (unchanged; keeps initial→full, birthday, nick, sex upgrades) --- +func (s *Store) mergeUserFields(id uint64, in *model.User) *model.User { + ex := s.users[id] + // fathersname: initial -> full (same initial), move indexes + if ex.Fathersname != "" && len([]rune(ex.Fathersname)) == 1 && + in.Fathersname != "" && fatherInitial(ex.Fathersname) == fatherInitial(in.Fathersname) && + ex.Fathersname != in.Fathersname { + oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)} + if _, ok := s.nameToUID[oldNameKey]; ok { + delete(s.nameToUID, oldNameKey) + } + delUserInitIndex(s.nameInitToUID, ex) + ex.Fathersname = in.Fathersname + s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id + addUserInitIndex(s.nameInitToUID, ex) + } + // birthday upgrade + if ymdUTC(ex.Birthday) == 0 && ymdUTC(in.Birthday) != 0 { + oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: 0} + if _, ok := s.nameToUID[oldNameKey]; ok { + delete(s.nameToUID, oldNameKey) + } + delUserInitIndex(s.nameInitToUID, ex) + ex.Birthday = in.Birthday + s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id + addUserInitIndex(s.nameInitToUID, ex) + } + // nick/sex + if ex.Nick == "" && in.Nick != "" { + ex.Nick = in.Nick + s.nickToUID[in.Nick] = id + } + if ex.Sex == model.SexUnknown && in.Sex != model.SexUnknown { + ex.Sex = in.Sex + } + return ex +} + +// --- FIXED SaveUser: initial-key lookup tries BOTH (birth, 0) --- +func (s *Store) SaveUser(u *model.User) (*model.User, error) { + if u == nil { + return nil, errors.New("nil user") + } + // normalize (names already UPPER) + u.Nick = strings.TrimSpace(u.Nick) + u.Name = strings.TrimSpace(u.Name) + u.Surname = strings.TrimSpace(u.Surname) + u.Fathersname = strings.Trim(strings.TrimSpace(u.Fathersname), ".,") + if u.Birthday.IsZero() { + u.Birthday = model.SentinelBirthday() + } + inBirth := ymdUTC(u.Birthday) + inKey := userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: inBirth} + + s.mu.Lock() + defer s.mu.Unlock() + + // 1) by Nick + if u.Nick != "" { + if id, ok := s.nickToUID[u.Nick]; ok { + return s.mergeUserFields(id, u), nil + } + } + + // 2) exact tuple + if id, ok := s.nameToUID[inKey]; ok { + if u.Nick != "" && s.users[id].Nick == "" { + s.users[id].Nick = u.Nick + s.nickToUID[u.Nick] = id + } + return s.mergeUserFields(id, u), nil + } + + // 3) initial-based match (try with incoming birth, then with 0) + init := fatherInitial(u.Fathersname) + tryInits := []userInitKey{ + {Surname: u.Surname, Name: u.Name, Init: init, BirthYMD: inBirth}, + } + if inBirth != 0 { + tryInits = append(tryInits, userInitKey{Surname: u.Surname, Name: u.Name, Init: init, BirthYMD: 0}) + } + for _, ik := range tryInits { + if id, ok := s.nameInitToUID[ik]; ok { + ex := s.users[id] + + // If ex has initial-only and incoming has full (same initial) → upgrade fathers + move indexes + if ex.Fathersname == fatherInitial(ex.Fathersname) && + u.Fathersname != "" && + fatherInitial(u.Fathersname) == fatherInitial(ex.Fathersname) { + // move name index + oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)} + delete(s.nameToUID, oldNameKey) + // remove old init index (birth may differ) + delUserInitIndex(s.nameInitToUID, ex) + + ex.Fathersname = u.Fathersname + + newNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)} + s.nameToUID[newNameKey] = id + addUserInitIndex(s.nameInitToUID, ex) + } + + // Upgrade birthday if needed + if ymdUTC(ex.Birthday) == 0 && inBirth != 0 { + // move name key 0 -> inBirth + oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: 0} + if _, ok2 := s.nameToUID[oldNameKey]; ok2 { + delete(s.nameToUID, oldNameKey) + } + // move init index 0 -> inBirth + delUserInitIndex(s.nameInitToUID, ex) + ex.Birthday = u.Birthday + s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: inBirth}] = id + addUserInitIndex(s.nameInitToUID, ex) + } + + // nick/sex upgrades + if u.Nick != "" && ex.Nick == "" { + ex.Nick = u.Nick + s.nickToUID[u.Nick] = id + } + if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { + ex.Sex = u.Sex + } + return ex, nil + } + } + + // 4) relaxed: fathersname empty, same birth + if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: "", BirthYMD: inBirth}]; ok { + ex := s.users[id] + if ex.Fathersname == "" && u.Fathersname != "" { + delete(s.nameToUID, userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: "", BirthYMD: inBirth}) + ex.Fathersname = u.Fathersname + s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: inBirth}] = id + addUserInitIndex(s.nameInitToUID, ex) + } + if u.Nick != "" && ex.Nick == "" { + ex.Nick = u.Nick + s.nickToUID[u.Nick] = id + } + if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { + ex.Sex = u.Sex + } + if ymdUTC(ex.Birthday) == 0 && inBirth != 0 { + ex.Birthday = u.Birthday + } + return ex, nil + } + + // 5) same fathersname, no birth + if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: 0}]; ok { + delete(s.nameToUID, userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: 0}) + ex := s.users[id] + ex.Birthday = u.Birthday + s.nameToUID[inKey] = id + delUserInitIndex(s.nameInitToUID, ex) + addUserInitIndex(s.nameInitToUID, ex) + if u.Nick != "" && ex.Nick == "" { + ex.Nick = u.Nick + s.nickToUID[u.Nick] = id + } + if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { + ex.Sex = u.Sex + } + return ex, nil + } + + // 6) fully unspecific existing (fathers="", birth=0) + if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: "", BirthYMD: 0}]; ok { + ex := s.users[id] + delete(s.nameToUID, userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: "", BirthYMD: 0}) + if ex.Fathersname == "" && u.Fathersname != "" { + ex.Fathersname = u.Fathersname + } + if ymdUTC(ex.Birthday) == 0 && inBirth != 0 { + ex.Birthday = u.Birthday + } + s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id + addUserInitIndex(s.nameInitToUID, ex) + if u.Nick != "" && ex.Nick == "" { + ex.Nick = u.Nick + s.nickToUID[u.Nick] = id + } + if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown { + ex.Sex = u.Sex + } + return ex, nil + } + + // 7) create + u.ID = uint64(len(s.users)) + s.users = append(s.users, u) + if u.Nick != "" { + s.nickToUID[u.Nick] = u.ID + } + s.nameToUID[inKey] = u.ID + addUserInitIndex(s.nameInitToUID, u) + return u, nil +} + +/* ============================== cards ============================= */ +/* +Match order: + 1) exact (Prefix, Number, Bonus) + 2) pair (Prefix, Number) → if stored bonus=="" and incoming bonus!="", upgrade in place (move triple index) + 3) else create new +Never steal UserID: only set if existing has 0 and incoming non-zero. +*/ + +func (s *Store) SaveCard(c *model.Card) (*model.Card, error) { + if c == nil { + return nil, errors.New("nil card") + } + c.Prefix = strings.TrimSpace(c.Prefix) + c.Bonusprogramm = strings.TrimSpace(c.Bonusprogramm) + if c.Prefix == "" { + return nil, errors.New("invalid card: empty prefix") + } + + tri := cardKey{Prefix: c.Prefix, Number: c.Number, Bonus: c.Bonusprogramm} + pair := cardPairKey{Prefix: c.Prefix, Number: c.Number} + + s.mu.Lock() + defer s.mu.Unlock() + + // exact triple + if id, ok := s.cardToCID[tri]; ok { + ex := s.cards[id] + if ex.UserID == 0 && c.UserID != 0 { + ex.UserID = c.UserID + if s.cardsByUser[ex.UserID] == nil { + s.cardsByUser[ex.UserID] = make(map[uint64]struct{}, 1024) + } + v := s.cardsByUser[ex.UserID] + v[ex.ID] = struct{}{} + s.cardsByUser[ex.UserID] = v + } + return ex, nil + } + + // by pair + if id, ok := s.cardPairToCID[pair]; ok { + ex := s.cards[id] + // link user once + if ex.UserID == 0 && c.UserID != 0 { + ex.UserID = c.UserID + } + if s.cardsByUser[ex.UserID] == nil { + s.cardsByUser[ex.UserID] = make(map[uint64]struct{}, 1024) + } + v := s.cardsByUser[ex.UserID] + v[ex.ID] = struct{}{} + s.cardsByUser[ex.UserID] = v + switch { + case ex.Bonusprogramm == "" && c.Bonusprogramm != "": + // move triple index from empty -> new bonus + oldTri := cardKey{Prefix: ex.Prefix, Number: ex.Number, Bonus: ex.Bonusprogramm} + delete(s.cardToCID, oldTri) + ex.Bonusprogramm = c.Bonusprogramm + newTri := cardKey{Prefix: ex.Prefix, Number: ex.Number, Bonus: ex.Bonusprogramm} + s.cardToCID[newTri] = id + return ex, nil + case ex.Bonusprogramm == "" && c.Bonusprogramm == "": + return ex, nil + case ex.Bonusprogramm != "" && c.Bonusprogramm == "": + return ex, nil + case ex.Bonusprogramm != "" && c.Bonusprogramm != "" && ex.Bonusprogramm != c.Bonusprogramm: + // different program → create new card record + default: + return ex, nil + } + } + + // create + c.ID = uint64(len(s.cards)) + s.cards = append(s.cards, c) + s.cardPairToCID[pair] = c.ID + s.cardToCID[tri] = c.ID // even if bonus == "", we still index triple + + if s.cardsByUser[c.UserID] == nil { + s.cardsByUser[c.UserID] = make(map[uint64]struct{}, 1024) + } + v := s.cardsByUser[c.UserID] + v[c.ID] = struct{}{} + s.cardsByUser[c.UserID] = v + + return c, nil +} + +/* ============================== flights =========================== */ +/* +Identity: + - date-only: (Number, From, To, DateYMD, false, 0) + - timed : (Number, From, To, DateYMD, true, SecSinceMidnight) +Upgrade: + - if a date-only exists and a timed arrives for the same day, upgrade in place +Merge: + - coords: fill when missing + - relations: add (dedup via sets) +*/ + +func (s *Store) SaveFlight(f *model.Flight) (*model.Flight, error) { + if f == nil { + return nil, errors.New("nil flight") + } + f.Number = strings.TrimSpace(f.Number) + f.From = strings.TrimSpace(f.From) + f.To = strings.TrimSpace(f.To) + + // normalize day + dayUTC := time.Date(f.Date.Year(), f.Date.Month(), f.Date.Day(), 0, 0, 0, 0, time.UTC) + ymd := ymdUTC(dayUTC) + + var pKey flightKey + if f.HasTime { + pKey = flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(f.Date)} + } else { + f.Date = dayUTC // store as date-only + pKey = flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: false, Sec: 0} + } + dayKey := flightDayKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd} + + s.mu.Lock() + defer s.mu.Unlock() + + // 1) exact (precise) key + if id, ok := s.flightToFID[pKey]; ok { + ex := s.flights[id] + s.mergeFlightFields(id, ex, f) + return ex, nil + } + + // 2) same day exists -> maybe upgrade date-only to timed + if id, ok := s.flightByDay[dayKey]; ok { + ex := s.flights[id] + exKey := s.keyOfFlight(ex) + if !ex.HasTime && f.HasTime { + // move map key to timed + delete(s.flightToFID, exKey) + ex.HasTime = true + // set clock from incoming (keep same calendar date) + ex.Date = time.Date(dayUTC.Year(), dayUTC.Month(), dayUTC.Day(), + f.Date.Hour(), f.Date.Minute(), f.Date.Second(), f.Date.Nanosecond(), f.Date.Location()) + s.flightToFID[s.keyOfFlight(ex)] = id + // day index already points to best precision + } + // merge fields/relations + s.mergeFlightFields(id, ex, f) + return ex, nil + } + + // 3) brand new + f.ID = uint64(len(s.flights)) + s.flights = append(s.flights, f) + s.flightToFID[pKey] = f.ID + s.flightByDay[dayKey] = f.ID + + // if s.countriesByUser[f.UserID] == nil { + // s.countriesByUser[f.UserID] = make(map[string]struct{}, 1024) + // } + + // v := s.countriesByUser[f.UserID] + // dd, _ := airports.LookupIATA(f.From) + // v[dd.Country] = struct{}{} + // s.countriesByUser[f.UserID] = v + + if f.Code != "" { + if s.codesByUser[f.UserID] == nil { + s.codesByUser[f.UserID] = make(map[string]struct{}, 1024) + } + codesByUser := s.codesByUser[f.UserID] + codesByUser[f.Code] = struct{}{} + s.codesByUser[f.UserID] = codesByUser + } + + // relations + if f.UserID != 0 { + ensureSet(s.userFlights, f.UserID)[f.ID] = struct{}{} + } + if f.CardID != 0 { + ensureSet(s.cardFlights, f.CardID)[f.ID] = struct{}{} + } + return f, nil +} + +func (s *Store) keyOfFlight(f *model.Flight) flightKey { + ymd := ymdUTC(time.Date(f.Date.Year(), f.Date.Month(), f.Date.Day(), 0, 0, 0, 0, time.UTC)) + if f.HasTime { + return flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(f.Date)} + } + return flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: false, Sec: 0} +} + +func (s *Store) mergeFlightFields(id uint64, ex, in *model.Flight) { + // coords: fill when empty + if isZeroCoord(ex.FromCoords) && !isZeroCoord(in.FromCoords) { + ex.FromCoords = in.FromCoords + } + if isZeroCoord(ex.ToCoords) && !isZeroCoord(in.ToCoords) { + ex.ToCoords = in.ToCoords + } + // relations + if in.UserID != 0 { + ensureSet(s.userFlights, in.UserID)[id] = struct{}{} + } + if in.CardID != 0 { + ensureSet(s.cardFlights, in.CardID)[id] = struct{}{} + } + if in.Code != "" && ex.Code == "" { + ex.Code = in.Code + + // if s.codesByUser[in.UserID] == nil { + // s.codesByUser[in.UserID] = make(map[string]struct{}, 1024) + // } + // codesByUser := s.codesByUser[in.UserID] + // codesByUser[in.Code] = struct{}{} + // s.codesByUser[in.UserID] = codesByUser + } +} + +/* ============================== finders =========================== */ + +func (s *Store) FindUserByNick(nick string) (*model.User, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + id, ok := s.nickToUID[strings.TrimSpace(nick)] + if !ok || id == 0 || int(id) >= len(s.users) { + return nil, false + } + return s.users[id], true +} + +func (s *Store) FindUserByName(name, surname, fathers string, bday time.Time) (*model.User, bool) { + key := userNameKey{ + Surname: strings.TrimSpace(surname), + Name: strings.TrimSpace(name), + Fathersname: strings.TrimSpace(fathers), + BirthYMD: ymdUTC(bday), + } + s.mu.RLock() + defer s.mu.RUnlock() + id, ok := s.nameToUID[key] + if !ok || id == 0 || int(id) >= len(s.users) { + return nil, false + } + return s.users[id], true +} + +func (s *Store) FindCard(prefix string, number uint64, bonus string) (*model.Card, bool) { + tri := cardKey{Prefix: strings.TrimSpace(prefix), Number: number, Bonus: strings.TrimSpace(bonus)} + s.mu.RLock() + defer s.mu.RUnlock() + if id, ok := s.cardToCID[tri]; ok && id != 0 && int(id) < len(s.cards) { + return s.cards[id], true + } + // fall back to pair if no exact + pair := cardPairKey{Prefix: strings.TrimSpace(prefix), Number: number} + if id, ok := s.cardPairToCID[pair]; ok && id != 0 && int(id) < len(s.cards) { + return s.cards[id], true + } + return nil, false +} + +func (s *Store) FindFlight(number, from, to string, date time.Time, hasTime bool) (*model.Flight, bool) { + number = strings.TrimSpace(number) + from = strings.TrimSpace(from) + to = strings.TrimSpace(to) + + ymd := ymdUTC(time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)) + var k flightKey + if hasTime { + k = flightKey{Number: number, From: from, To: to, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(date)} + } else { + k = flightKey{Number: number, From: from, To: to, DateYMD: ymd, HasTime: false, Sec: 0} + } + + s.mu.RLock() + defer s.mu.RUnlock() + if id, ok := s.flightToFID[k]; ok && id != 0 && int(id) < len(s.flights) { + return s.flights[id], true + } + // day-level fallback (returns best precision for the day if exact key absent) + if id, ok := s.flightByDay[flightDayKey{Number: number, From: from, To: to, DateYMD: ymd}]; ok && id != 0 && int(id) < len(s.flights) { + return s.flights[id], true + } + return nil, false +} -- cgit v1.2.3