forked from TrueCloudLab/frostfs-node
[#316] locode: Implement csv UN/LOCODE table
Define csv UN/LOCODE table. Implement iterator over table entries. Implement method that resolves country/subdivision code pair to subdivision name. Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
parent
cdd1274e1c
commit
6829048124
3 changed files with 218 additions and 0 deletions
120
pkg/util/locode/table/csv/calls.go
Normal file
120
pkg/util/locode/table/csv/calls.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
package csvlocode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neofs-node/pkg/util/locode"
|
||||||
|
locodedb "github.com/nspcc-dev/neofs-node/pkg/util/locode/db"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errInvalidRecord = errors.New("invalid table record")
|
||||||
|
|
||||||
|
// IterateAll scans table record one-by-one, parses UN/LOCODE record
|
||||||
|
// from it and passes it to f.
|
||||||
|
//
|
||||||
|
// Returns f's errors directly.
|
||||||
|
func (t *Table) IterateAll(f func(locode.Record) error) error {
|
||||||
|
const wordsPerRecord = 12
|
||||||
|
|
||||||
|
return t.scanWords(t.paths, wordsPerRecord, func(words []string) error {
|
||||||
|
lc, err := locode.FromString(strings.Join(words[1:3], " "))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
record := locode.Record{
|
||||||
|
Ch: words[0],
|
||||||
|
LOCODE: *lc,
|
||||||
|
Name: words[3],
|
||||||
|
NameWoDiacritics: words[4],
|
||||||
|
SubDiv: words[5],
|
||||||
|
Function: words[6],
|
||||||
|
Status: words[7],
|
||||||
|
Date: words[8],
|
||||||
|
IATA: words[9],
|
||||||
|
Coordinates: words[10],
|
||||||
|
Remarks: words[11],
|
||||||
|
}
|
||||||
|
|
||||||
|
return f(record)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubDivName scans table record one-by-one, and returns subdivision name
|
||||||
|
// on country and subdivision codes match.
|
||||||
|
//
|
||||||
|
// Returns locodedb.ErrSubDivNotFound if no entry matches.
|
||||||
|
func (t *Table) SubDivName(countryCode *locodedb.CountryCode, name string) (subDiv string, err error) {
|
||||||
|
const wordsPerRecord = 4
|
||||||
|
|
||||||
|
err = t.scanWords([]string{t.subDivPath}, wordsPerRecord, func(words []string) error {
|
||||||
|
if words[0] == countryCode.String() && words[1] == name {
|
||||||
|
subDiv = words[2]
|
||||||
|
return errScanInt
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil && subDiv == "" {
|
||||||
|
err = locodedb.ErrSubDivNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var errScanInt = errors.New("interrupt scan")
|
||||||
|
|
||||||
|
func (t *Table) scanWords(paths []string, fpr int, wordsHandler func([]string) error) error {
|
||||||
|
var (
|
||||||
|
rdrs = make([]io.Reader, 0, len(t.paths))
|
||||||
|
closers = make([]io.Closer, 0, len(t.paths))
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := range paths {
|
||||||
|
file, err := os.OpenFile(paths[i], os.O_RDONLY, t.mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rdrs = append(rdrs, file)
|
||||||
|
closers = append(closers, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
for i := range closers {
|
||||||
|
_ = closers[i].Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
r := csv.NewReader(io.MultiReader(rdrs...))
|
||||||
|
r.ReuseRecord = true
|
||||||
|
r.FieldsPerRecord = fpr
|
||||||
|
|
||||||
|
for {
|
||||||
|
words, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
} else if len(words) != fpr {
|
||||||
|
return errInvalidRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wordsHandler(words); err != nil {
|
||||||
|
if errors.Is(err, errScanInt) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
28
pkg/util/locode/table/csv/opts.go
Normal file
28
pkg/util/locode/table/csv/opts.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package csvlocode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option sets an optional parameter of Table.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
mode os.FileMode
|
||||||
|
|
||||||
|
extraPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultOpts() *options {
|
||||||
|
return &options{
|
||||||
|
mode: 0700,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExtraPaths returns option to add extra paths
|
||||||
|
// to UN/LOCODE tables in csv format.
|
||||||
|
func WithExtraPaths(ps ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.extraPaths = append(o.extraPaths, ps...)
|
||||||
|
}
|
||||||
|
}
|
70
pkg/util/locode/table/csv/table.go
Normal file
70
pkg/util/locode/table/csv/table.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package csvlocode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prm groups the required parameters of the Table's constructor.
|
||||||
|
//
|
||||||
|
// All values must comply with the requirements imposed on them.
|
||||||
|
// Passing incorrect parameter values will result in constructor
|
||||||
|
// failure (error or panic depending on the implementation).
|
||||||
|
type Prm struct {
|
||||||
|
// Path to UN/LOCODE csv table.
|
||||||
|
//
|
||||||
|
// Must not be empty.
|
||||||
|
Path string
|
||||||
|
|
||||||
|
// Path to csv table of UN/LOCODE Subdivisions.
|
||||||
|
//
|
||||||
|
// Must not be empty.
|
||||||
|
SubDivPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table is a descriptor of the UN/LOCODE table in csv format.
|
||||||
|
//
|
||||||
|
// For correct operation, Table must be created
|
||||||
|
// using the constructor (New) based on the required parameters
|
||||||
|
// and optional components. After successful creation,
|
||||||
|
// The Table is immediately ready to work through API.
|
||||||
|
type Table struct {
|
||||||
|
paths []string
|
||||||
|
|
||||||
|
mode os.FileMode
|
||||||
|
|
||||||
|
subDivPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidPrmValFmt = "invalid parameter %s (%T):%v"
|
||||||
|
|
||||||
|
func panicOnPrmValue(n string, v interface{}) {
|
||||||
|
panic(fmt.Sprintf(invalidPrmValFmt, n, v, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new instance of the Table.
|
||||||
|
//
|
||||||
|
// Panics if at least one value of the parameters is invalid.
|
||||||
|
//
|
||||||
|
// The created Table does not require additional
|
||||||
|
// initialization and is completely ready for work.
|
||||||
|
func New(prm Prm, opts ...Option) *Table {
|
||||||
|
switch {
|
||||||
|
case prm.Path == "":
|
||||||
|
panicOnPrmValue("Path", prm.Path)
|
||||||
|
case prm.SubDivPath == "":
|
||||||
|
panicOnPrmValue("SubDivPath", prm.SubDivPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
o := defaultOpts()
|
||||||
|
|
||||||
|
for i := range opts {
|
||||||
|
opts[i](o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Table{
|
||||||
|
paths: append(o.extraPaths, prm.Path),
|
||||||
|
mode: o.mode,
|
||||||
|
subDivPath: prm.SubDivPath,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue