diff --git a/internal/parsers/db/airports/calls.go b/internal/parsers/db/airports/calls.go index 96137a7..63a7359 100644 --- a/internal/parsers/db/airports/calls.go +++ b/internal/parsers/db/airports/calls.go @@ -94,15 +94,13 @@ const ( // and returns the name of the country by code. // // Returns locodedb.ErrCountryNotFound if no entry matches. -func (db *DB) CountryName(code *locodedb.CountryCode) (name string, err error) { +func (db *DB) CountryName(code string) (name string, err error) { if err = db.initCountries(); err != nil { return } - argCode := code.String() - for cName, cCode := range db.mCountries { - if cCode == argCode { + if cCode == code { name = cName break } diff --git a/internal/parsers/db/db.go b/internal/parsers/db/db.go index f626bbc..f18f4f6 100644 --- a/internal/parsers/db/db.go +++ b/internal/parsers/db/db.go @@ -54,14 +54,14 @@ type NamesDB interface { // // Must return ErrCountryNotFound if there is no // country with the provided code. - CountryName(*locodedb.CountryCode) (string, error) + CountryName(string) (string, error) // SubDivName must resolve (country code, subdivision code) to // a subdivision name. // // Must return ErrSubDivNotFound if either country or // subdivision is not presented in database. - SubDivName(*locodedb.CountryCode, string) (string, error) + SubDivName(string, string) (string, error) } // FillDatabase generates the location database based on the UN/LOCODE table. @@ -72,7 +72,7 @@ func FillDatabase(table SourceTable, airports AirportDB, continents ContinentsDB return nil } - dbKey, err := locodedb.NewKey(tableRecord.LOCODE[0], tableRecord.LOCODE[1]) + dbKey, err := NewKey(tableRecord.LOCODE[0], tableRecord.LOCODE[1]) if err != nil { return err } diff --git a/internal/parsers/db/key.go b/internal/parsers/db/key.go new file mode 100644 index 0000000..630fdc1 --- /dev/null +++ b/internal/parsers/db/key.go @@ -0,0 +1,64 @@ +package locodedb + +import ( + "fmt" + + "github.com/nspcc-dev/locode-db/pkg/locodedb" +) + +// Key represents the Key in location database. It contains the country code and the location code. +type Key struct { + cc string + lc string +} + +// NewKey returns a new Key from a country code and a location code string pair (e.g. "USNYC") or an error if +// the string is invalid. The country code must be 2 letters long and the location code 3 letters long. +func NewKey(country, location string) (*Key, error) { + err := validateCode(country, locodedb.CountryCodeLen) + if err != nil { + return nil, fmt.Errorf("could not parse country: %w", err) + } + + err = validateCode(location, locodedb.LocationCodeLen) + if err != nil { + return nil, fmt.Errorf("could not parse location: %w", err) + } + + return &Key{ + cc: country, + lc: location, + }, nil +} + +// CountryCode returns the location's country code in string representation. +func (k *Key) CountryCode() string { + return k.cc +} + +// LocationCode returns the location code in string representation. +func (k *Key) LocationCode() string { + return k.lc +} + +// validateCode validates if code is. +func validateCode(s string, codeLen int) error { + if l := len(s); l != codeLen { + return fmt.Errorf("incorrect location code length: expect: %d, got: %d", + codeLen, + l, + ) + } + + for i := range s { + if !isUpperAlpha(s[i]) && !isDigit(s[i]) { + return locodedb.ErrInvalidString + } + } + + return nil +} + +func isUpperAlpha(sym uint8) bool { + return sym >= 'A' && sym <= 'Z' +} diff --git a/internal/parsers/db/put.go b/internal/parsers/db/put.go index 4878325..45a35d0 100644 --- a/internal/parsers/db/put.go +++ b/internal/parsers/db/put.go @@ -23,7 +23,7 @@ const ( // Data is a struct that contains the Key and the Record. type Data struct { - Key locodedb.Key + Key Key Record locodedb.Record } @@ -40,7 +40,7 @@ func (db *CsvDB) Put(data []Data) error { rec := row.Record // Calculate a unique index for each key - keyString := key.CountryCode().String() + key.LocationCode().String() + keyString := key.CountryCode() + key.LocationCode() if index, exists := uniqueKeys[keyString]; exists { // We expected duplicates from override.csv to override wrong number format in location @@ -64,14 +64,14 @@ func (db *CsvDB) Put(data []Data) error { newRecordsLocode = append(newRecordsLocode, newRecord) - if _, exists := uniqueKeysCountry[key.CountryCode().String()]; exists { + if _, exists := uniqueKeysCountry[key.CountryCode()]; exists { continue } - uniqueKeysCountry[key.CountryCode().String()] = struct{}{} + uniqueKeysCountry[key.CountryCode()] = struct{}{} newRecordCountry := []string{ - key.CountryCode().String(), + key.CountryCode(), rec.Country, } diff --git a/internal/parsers/table/csv/calls.go b/internal/parsers/table/csv/calls.go index 1e2df62..1e9d98d 100644 --- a/internal/parsers/table/csv/calls.go +++ b/internal/parsers/table/csv/calls.go @@ -9,7 +9,6 @@ import ( "unicode/utf8" locode "github.com/nspcc-dev/locode-db/internal/parsers/db" - "github.com/nspcc-dev/locode-db/pkg/locodedb" "golang.org/x/text/encoding/charmap" ) @@ -67,13 +66,13 @@ type subDivRecord struct { // and returns the subdivision name of the country and the subdivision codes match. // // Returns locodedb.ErrSubDivNotFound if no entry matches. -func (t *Table) SubDivName(countryCode *locodedb.CountryCode, code string) (string, error) { +func (t *Table) SubDivName(countryCode string, code string) (string, error) { if err := t.initSubDiv(); err != nil { return "", err } rec, ok := t.mSubDiv[subDivKey{ - countryCode: countryCode.String(), + countryCode: countryCode, subDivCode: code, }] if !ok { diff --git a/pkg/locodedb/calls.go b/pkg/locodedb/calls.go index 34a8e39..2a0436e 100644 --- a/pkg/locodedb/calls.go +++ b/pkg/locodedb/calls.go @@ -15,7 +15,7 @@ var ( locodeStrings string // mCountries is a map of country codes to country names and locodes. - mCountries map[CountryCode]countryData + mCountries map[countryCode]countryData ) // Get returns a record for a given locode string. The string must be 5 or 6 @@ -44,7 +44,7 @@ func Get(locodeStr string) (Record, error) { } } - cc := CountryCode{} + cc := countryCode{} copy(cc[:], locodeStr[:2]) cd, countryFound := mCountries[cc] if !countryFound { @@ -53,9 +53,9 @@ func Get(locodeStr string) (Record, error) { code := locodeStr[CountryCodeLen:] n, _ := slices.BinarySearchFunc(cd.locodes, code, func(csv locodesCSV, s string) int { - return cmp.Compare(codeFromCSV(&csv), s) + return cmp.Compare(csv.code, s) }) - if n == len(cd.locodes) || strings.Compare(codeFromCSV(&cd.locodes[n]), code) != 0 { + if n == len(cd.locodes) || strings.Compare(cd.locodes[n].code, code) != 0 { return Record{}, ErrNotFound } @@ -69,10 +69,6 @@ func Get(locodeStr string) (Record, error) { }, nil } -func codeFromCSV(c *locodesCSV) string { - return string(c.code[:]) -} - func locFromCSV(c *locodesCSV) string { return locodeStrings[c.offset : c.offset+uint32(c.locationLen)] } diff --git a/pkg/locodedb/calls_test.go b/pkg/locodedb/calls_test.go index 8d00e45..afd14d6 100644 --- a/pkg/locodedb/calls_test.go +++ b/pkg/locodedb/calls_test.go @@ -65,8 +65,4 @@ func TestGet(t *testing.T) { require.Equal(t, utf8.ValidString(rec.SubDivName), true) }) }) - t.Run("valid key", func(t *testing.T) { - _, err := locodedb.NewKey("RU", "MOW") - require.NoError(t, err) - }) } diff --git a/pkg/locodedb/country.go b/pkg/locodedb/country.go index 33cf7bb..85b388a 100644 --- a/pkg/locodedb/country.go +++ b/pkg/locodedb/country.go @@ -1,17 +1,24 @@ package locodedb import ( + "errors" "fmt" ) // CountryCodeLen is the length of the country code. const CountryCodeLen = 2 -// CountryCode represents ISO 3166 alpha-2 Country Code. -type CountryCode [CountryCodeLen]uint8 +// LocationCodeLen is the length of the location code. +const LocationCodeLen = 3 -// CountryCodeFromString parses a string and returns the country code. -func CountryCodeFromString(s string) (*CountryCode, error) { +// ErrInvalidString is returned when the string is not a valid location code. +var ErrInvalidString = errors.New("invalid string format in UN/Locode") + +// countryCode represents ISO 3166 alpha-2 Country Code. +type countryCode [CountryCodeLen]uint8 + +// countryCodeFromString parses a string and returns the country code. +func countryCodeFromString(s string) (*countryCode, error) { if l := len(s); l != CountryCodeLen { return nil, fmt.Errorf("incorrect country code length: expect: %d, got: %d", CountryCodeLen, @@ -25,19 +32,16 @@ func CountryCodeFromString(s string) (*CountryCode, error) { } } - cc := CountryCode{} + cc := countryCode{} copy(cc[:], s) return &cc, nil } -// String returns a string representation of the country code. -func (c *CountryCode) String() string { - syms := c.Symbols() - return string(syms[:]) +func isUpperAlpha(sym uint8) bool { + return sym >= 'A' && sym <= 'Z' } -// Symbols returns the country code as a slice of symbols. -func (c *CountryCode) Symbols() [CountryCodeLen]uint8 { - return *c +func isDigit(sym uint8) bool { + return sym >= '0' && sym <= '9' } diff --git a/pkg/locodedb/location.go b/pkg/locodedb/location.go deleted file mode 100644 index 29a4317..0000000 --- a/pkg/locodedb/location.go +++ /dev/null @@ -1,56 +0,0 @@ -package locodedb - -import ( - "errors" - "fmt" -) - -// LocationCodeLen is the length of the location code. -const LocationCodeLen = 3 - -// ErrInvalidString is returned when the string is not a valid location code. -var ErrInvalidString = errors.New("invalid string format in UN/Locode") - -// LocationCode represents a location code for -// the storage in the location database. -type LocationCode [LocationCodeLen]uint8 - -// LocationCodeFromString parses a string and returns the location code. -func LocationCodeFromString(s string) (*LocationCode, error) { - if l := len(s); l != LocationCodeLen { - return nil, fmt.Errorf("incorrect location code length: expect: %d, got: %d", - LocationCodeLen, - l, - ) - } - - for i := range s { - if !isUpperAlpha(s[i]) && !isDigit(s[i]) { - return nil, ErrInvalidString - } - } - - lc := LocationCode{} - copy(lc[:], s) - - return &lc, nil -} - -func isDigit(sym uint8) bool { - return sym >= '0' && sym <= '9' -} - -func isUpperAlpha(sym uint8) bool { - return sym >= 'A' && sym <= 'Z' -} - -// String returns a string representation of the location code. -func (l *LocationCode) String() string { - syms := l.Symbols() - return string(syms[:]) -} - -// Symbols returns the location code as a slice of symbols. -func (l *LocationCode) Symbols() [LocationCodeLen]uint8 { - return *l -} diff --git a/pkg/locodedb/record.go b/pkg/locodedb/record.go index ad8f3d7..43ebee3 100644 --- a/pkg/locodedb/record.go +++ b/pkg/locodedb/record.go @@ -1,44 +1,5 @@ package locodedb -import ( - "fmt" -) - -// Key represents the key in location database. It contains the country code and the location code. -type Key struct { - cc *CountryCode - lc *LocationCode -} - -// NewKey returns a new Key from a country code and a location code string pair (e.g. "USNYC") or an error if -// the string is invalid. The country code must be 2 letters long and the location code 3 letters long. -func NewKey(country, location string) (*Key, error) { - countryCode, err := CountryCodeFromString(country) - if err != nil { - return nil, fmt.Errorf("could not parse country: %w", err) - } - - locationCode, err := LocationCodeFromString(location) - if err != nil { - return nil, fmt.Errorf("could not parse location: %w", err) - } - - return &Key{ - cc: countryCode, - lc: locationCode, - }, nil -} - -// CountryCode returns the location's country code. -func (k *Key) CountryCode() *CountryCode { - return k.cc -} - -// LocationCode returns the location code. -func (k *Key) LocationCode() *LocationCode { - return k.lc -} - // Record represents a record in the location database (resulting CSV files). It contains all the // information about the location. It is used to fill the database. Country, Location are full names, codes are in Key. type Record struct { diff --git a/pkg/locodedb/utils.go b/pkg/locodedb/utils.go index 3ecc2e2..393a5f3 100644 --- a/pkg/locodedb/utils.go +++ b/pkg/locodedb/utils.go @@ -45,15 +45,15 @@ type countryData struct { type locodesCSV struct { point Point offset uint32 - code LocationCode + code string locationLen uint8 subDivCodeLen uint8 subDivNameLen uint8 continent Continent } -func unpackCountriesData(data []byte) (map[CountryCode]countryData, error) { - m := make(map[CountryCode]countryData) +func unpackCountriesData(data []byte) (map[countryCode]countryData, error) { + m := make(map[countryCode]countryData) zReader := bzip2.NewReader(bytes.NewReader(data)) reader := csv.NewReader(zReader) @@ -65,16 +65,16 @@ func unpackCountriesData(data []byte) (map[CountryCode]countryData, error) { } else if err != nil { return m, err } - countryCode, err := CountryCodeFromString(record[0]) + cc, err := countryCodeFromString(record[0]) if err != nil { return m, err } - m[*countryCode] = countryData{name: record[1]} + m[*cc] = countryData{name: record[1]} } return m, nil } -func unpackLocodesData(data []byte, mc map[CountryCode]countryData) (string, error) { +func unpackLocodesData(data []byte, mc map[countryCode]countryData) (string, error) { var ( b strings.Builder zReader = bzip2.NewReader(bytes.NewReader(data)) @@ -100,14 +100,12 @@ func unpackLocodesData(data []byte, mc map[CountryCode]countryData) (string, err return "", errors.New("string buffer int32 overflow") } var ( - code LocationCode recOffset = uint32(b.Len()) locationLen = uint8(len(record[1])) subDivCodeLen = uint8(len(record[3])) subDivNameLen = uint8(len(record[4])) ) - copy(code[:], record[0][CountryCodeLen:]) b.WriteString(record[1]) b.WriteString(record[3]) b.WriteString(record[4]) @@ -124,24 +122,24 @@ func unpackLocodesData(data []byte, mc map[CountryCode]countryData) (string, err return "", err } - countryCode, err := CountryCodeFromString(record[0][:CountryCodeLen]) + cc, err := countryCodeFromString(record[0][:CountryCodeLen]) if err != nil { return "", err } - rec, ok := mc[*countryCode] + rec, ok := mc[*cc] if !ok { return "", errors.New("invalid country in the DB") } rec.locodes = append(rec.locodes, locodesCSV{ point: Point{Latitude: float32(lat), Longitude: float32(lng)}, - code: code, + code: record[0][CountryCodeLen:], offset: recOffset, locationLen: locationLen, subDivCodeLen: subDivCodeLen, subDivNameLen: subDivNameLen, continent: continent, }) - mc[*countryCode] = rec + mc[*cc] = rec } for k := range mc { rec := mc[k]