From 6829048124eb04ef3a35d038391039cf0746f303 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 8 Feb 2021 20:40:36 +0300 Subject: [PATCH] [#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 --- pkg/util/locode/table/csv/calls.go | 120 +++++++++++++++++++++++++++++ pkg/util/locode/table/csv/opts.go | 28 +++++++ pkg/util/locode/table/csv/table.go | 70 +++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 pkg/util/locode/table/csv/calls.go create mode 100644 pkg/util/locode/table/csv/opts.go create mode 100644 pkg/util/locode/table/csv/table.go diff --git a/pkg/util/locode/table/csv/calls.go b/pkg/util/locode/table/csv/calls.go new file mode 100644 index 000000000..2a56723e9 --- /dev/null +++ b/pkg/util/locode/table/csv/calls.go @@ -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 +} diff --git a/pkg/util/locode/table/csv/opts.go b/pkg/util/locode/table/csv/opts.go new file mode 100644 index 000000000..b638d25d5 --- /dev/null +++ b/pkg/util/locode/table/csv/opts.go @@ -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...) + } +} diff --git a/pkg/util/locode/table/csv/table.go b/pkg/util/locode/table/csv/table.go new file mode 100644 index 000000000..43afd8428 --- /dev/null +++ b/pkg/util/locode/table/csv/table.go @@ -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, + } +}