From 0be35859ed7b8195ac7eb65569f0b55c07575c01 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 8 Feb 2021 19:44:35 +0300 Subject: [PATCH] [#316] locode: Define UN/LOCODE table data types Define the data types needed to work with LOCODE's in NeoFS (country code, location code, coordinates). Implement string parsers for new types. Signed-off-by: Leonard Lyubich --- pkg/util/locode/column/coordinates.go | 193 ++++++++++++++++++++++++++ pkg/util/locode/column/country.go | 33 +++++ pkg/util/locode/column/location.go | 33 +++++ pkg/util/locode/column/util.go | 9 ++ 4 files changed, 268 insertions(+) create mode 100644 pkg/util/locode/column/coordinates.go create mode 100644 pkg/util/locode/column/country.go create mode 100644 pkg/util/locode/column/location.go create mode 100644 pkg/util/locode/column/util.go diff --git a/pkg/util/locode/column/coordinates.go b/pkg/util/locode/column/coordinates.go new file mode 100644 index 000000000..bd887c33b --- /dev/null +++ b/pkg/util/locode/column/coordinates.go @@ -0,0 +1,193 @@ +package locodecolumn + +import ( + "strings" + + "github.com/nspcc-dev/neofs-node/pkg/util/locode" + "github.com/pkg/errors" +) + +const ( + minutesDigits = 2 + hemisphereSymbols = 1 +) + +const ( + latDegDigits = 2 + lngDegDigits = 3 +) + +type coordinateCode struct { + degDigits int + value []uint8 +} + +// LongitudeCode represents the value of the longitude +// of the location conforming to UN/LOCODE specification. +type LongitudeCode coordinateCode + +// LongitudeHemisphere represents hemisphere of the earth +// // along the Greenwich meridian. +type LongitudeHemisphere [hemisphereSymbols]uint8 + +// LatitudeCode represents the value of the latitude +// of the location conforming to UN/LOCODE specification. +type LatitudeCode coordinateCode + +// LatitudeHemisphere represents hemisphere of the earth +// along the equator. +type LatitudeHemisphere [hemisphereSymbols]uint8 + +func coordinateFromString(s string, degDigits int, hemisphereAlphabet []uint8) (*coordinateCode, error) { + if len(s) != degDigits+minutesDigits+hemisphereSymbols { + return nil, locode.ErrInvalidString + } + + for i := range s[:degDigits+minutesDigits] { + if !isDigit(s[i]) { + return nil, locode.ErrInvalidString + } + } + +loop: + for _, sym := range s[degDigits+minutesDigits:] { + for j := range hemisphereAlphabet { + if hemisphereAlphabet[j] == uint8(sym) { + continue loop + } + } + + return nil, locode.ErrInvalidString + } + + return &coordinateCode{ + degDigits: degDigits, + value: []uint8(s), + }, nil +} + +// LongitudeFromString parses string and returns location's longitude. +func LongitudeFromString(s string) (*LongitudeCode, error) { + cc, err := coordinateFromString(s, lngDegDigits, []uint8{'W', 'E'}) + if err != nil { + return nil, err + } + + return (*LongitudeCode)(cc), nil +} + +// LatitudeFromString parses string and returns location's latitude. +func LatitudeFromString(s string) (*LatitudeCode, error) { + cc, err := coordinateFromString(s, latDegDigits, []uint8{'N', 'S'}) + if err != nil { + return nil, err + } + + return (*LatitudeCode)(cc), nil +} + +func (cc *coordinateCode) degrees() []uint8 { + return cc.value[:cc.degDigits] +} + +// Degrees returns longitude's degrees. +func (lc *LongitudeCode) Degrees() (l [lngDegDigits]uint8) { + copy(l[:], (*coordinateCode)(lc).degrees()) + return +} + +// Degrees returns latitude's degrees. +func (lc *LatitudeCode) Degrees() (l [latDegDigits]uint8) { + copy(l[:], (*coordinateCode)(lc).degrees()) + return +} + +func (cc *coordinateCode) minutes() (mnt [minutesDigits]uint8) { + for i := 0; i < minutesDigits; i++ { + mnt[i] = cc.value[cc.degDigits+i] + } + + return +} + +// Minutes returns longitude's minutes. +func (lc *LongitudeCode) Minutes() [minutesDigits]uint8 { + return (*coordinateCode)(lc).minutes() +} + +// Minutes returns latitude's minutes. +func (lc *LatitudeCode) Minutes() [minutesDigits]uint8 { + return (*coordinateCode)(lc).minutes() +} + +// Hemisphere returns longitude's hemisphere code. +func (lc *LongitudeCode) Hemisphere() LongitudeHemisphere { + return (*coordinateCode)(lc).hemisphere() +} + +// Hemisphere returns latitude's hemisphere code. +func (lc *LatitudeCode) Hemisphere() LatitudeHemisphere { + return (*coordinateCode)(lc).hemisphere() +} + +func (cc *coordinateCode) hemisphere() (h [hemisphereSymbols]uint8) { + for i := 0; i < hemisphereSymbols; i++ { + h[i] = cc.value[cc.degDigits+minutesDigits+i] + } + + return h +} + +// North returns true for the northern hemisphere. +func (h LatitudeHemisphere) North() bool { + return h[0] == 'W' +} + +// East returns true for the eastern hemisphere. +func (h LongitudeHemisphere) East() bool { + return h[0] == 'E' +} + +// Coordinates represents coordinates of the location from UN/LOCODE table. +type Coordinates struct { + lat *LatitudeCode + + lng *LongitudeCode +} + +// Latitude returns location's latitude. +func (c *Coordinates) Latitude() *LatitudeCode { + return c.lat +} + +// Longitude returns location's longitude. +func (c *Coordinates) Longitude() *LongitudeCode { + return c.lng +} + +// CoordinatesFromString parses string and returns location's coordinates. +func CoordinatesFromString(s string) (*Coordinates, error) { + if len(s) == 0 { + return nil, nil + } + + strs := strings.Split(s, " ") + if len(strs) != 2 { + return nil, locode.ErrInvalidString + } + + lat, err := LatitudeFromString(strs[0]) + if err != nil { + return nil, errors.Wrap(err, "could not parse latitude") + } + + lng, err := LongitudeFromString(strs[1]) + if err != nil { + return nil, errors.Wrap(err, "could not parse longitude") + } + + return &Coordinates{ + lat: lat, + lng: lng, + }, nil +} diff --git a/pkg/util/locode/column/country.go b/pkg/util/locode/column/country.go new file mode 100644 index 000000000..adbff9f41 --- /dev/null +++ b/pkg/util/locode/column/country.go @@ -0,0 +1,33 @@ +package locodecolumn + +import ( + "github.com/nspcc-dev/neofs-node/pkg/util/locode" +) + +const countryCodeLen = 2 + +// CountryCode represents ISO 3166 alpha-2 Country Code. +type CountryCode [countryCodeLen]uint8 + +// Symbols returns digits of the country code. +func (cc *CountryCode) Symbols() [countryCodeLen]uint8 { + return *cc +} + +// CountryCodeFromString parses string and returns country code. +func CountryCodeFromString(s string) (*CountryCode, error) { + if len(s) != countryCodeLen { + return nil, locode.ErrInvalidString + } + + for i := range s { + if !isUpperAlpha(s[i]) { + return nil, locode.ErrInvalidString + } + } + + cc := CountryCode{} + copy(cc[:], s) + + return &cc, nil +} diff --git a/pkg/util/locode/column/location.go b/pkg/util/locode/column/location.go new file mode 100644 index 000000000..6d53af336 --- /dev/null +++ b/pkg/util/locode/column/location.go @@ -0,0 +1,33 @@ +package locodecolumn + +import ( + "github.com/nspcc-dev/neofs-node/pkg/util/locode" +) + +const locationCodeLen = 3 + +// LocationCode represents 3-character code for the location. +type LocationCode [locationCodeLen]uint8 + +// Symbols returns characters of the location code. +func (lc *LocationCode) Symbols() [locationCodeLen]uint8 { + return *lc +} + +// LocationCodeFromString parses string and returns location code. +func LocationCodeFromString(s string) (*LocationCode, error) { + if len(s) != locationCodeLen { + return nil, locode.ErrInvalidString + } + + for i := range s { + if !isUpperAlpha(s[i]) && !isDigit(s[i]) { + return nil, locode.ErrInvalidString + } + } + + lc := LocationCode{} + copy(lc[:], s) + + return &lc, nil +} diff --git a/pkg/util/locode/column/util.go b/pkg/util/locode/column/util.go new file mode 100644 index 000000000..8da1f9a25 --- /dev/null +++ b/pkg/util/locode/column/util.go @@ -0,0 +1,9 @@ +package locodecolumn + +func isDigit(sym uint8) bool { + return sym >= '0' && sym <= '9' +} + +func isUpperAlpha(sym uint8) bool { + return sym >= 'A' && sym <= 'Z' +}