From 8dad3924519ba2d841b536be8517fdd98238adfc Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 11 Feb 2024 15:50:43 +0300 Subject: [PATCH] handle dimension --- assets/gooditem.fbs | 7 ++ cmd/cli/dimensiondispatcher.go | 71 ++++++++++++ cmd/cli/main.go | 50 +++++---- go.mod | 5 + go.sum | 7 ++ internal/encoding/fbs/Dimensions.go | 49 ++++++++ internal/encoding/fbs/GoodItem.go | 28 ++++- internal/encoding/fbs/helpers.go | 20 ++++ internal/entity/dimension.go | 142 ++++++++++++++++++++++++ internal/entity/dimension_inner_test.go | 31 ++++++ internal/entity/dimension_test.go | 72 ++++++++++++ internal/entity/empty.go | 3 + internal/entity/gooditem.go | 21 ++++ 13 files changed, 479 insertions(+), 27 deletions(-) create mode 100644 cmd/cli/dimensiondispatcher.go create mode 100644 internal/encoding/fbs/Dimensions.go create mode 100644 internal/entity/dimension.go create mode 100644 internal/entity/dimension_inner_test.go create mode 100644 internal/entity/dimension_test.go create mode 100644 internal/entity/empty.go diff --git a/assets/gooditem.fbs b/assets/gooditem.fbs index 1bf2310..de572fc 100644 --- a/assets/gooditem.fbs +++ b/assets/gooditem.fbs @@ -1,5 +1,11 @@ namespace internal.encoding.fbs; +struct Dimensions { + width:float; + height:float; + length:float; +} + table GoodItem { sku:string; photo:string; @@ -14,6 +20,7 @@ table GoodItem { tariff:float; cart:long; stock:short; + sizes:Dimensions; parameters:string; created_at:long; } diff --git a/cmd/cli/dimensiondispatcher.go b/cmd/cli/dimensiondispatcher.go new file mode 100644 index 0000000..2d33783 --- /dev/null +++ b/cmd/cli/dimensiondispatcher.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "strings" + + "git.loyso.art/frx/eway/internal/entity" + "git.loyso.art/frx/eway/internal/matcher" + "github.com/rs/zerolog" +) + +type dimensionDispatcher struct { + heigth matcher.Unit + width matcher.Unit + length matcher.Unit +} + +func (d dimensionDispatcher) isDimensionParam(value string) bool { + return d.heigth.Match(value) || d.width.Match(value) || d.length.Match(value) +} + +func (d dimensionDispatcher) dispatch(ctx context.Context, value, key string, in *entity.GoodsItemSize) { + if !d.isDimensionParam(value) { + return + } + + if strings.Contains(value, "/") { + dimensionValues := strings.Split(value, "/") + for _, dv := range dimensionValues { + d.dispatch(ctx, dv, key, in) + } + } else { + out, err := entity.ParseDimention(key, entity.DimensionLocalRU) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("unable to parse key, skipping") + return + } + + out = out.AdjustTo(entity.DimensionKindCentimeter) + + switch { + case d.heigth.Match(value): + in.Height = out + case d.width.Match(value): + in.Width = out + case d.width.Match(value): + in.Length = out + default: + zerolog.Ctx(ctx).Error().Str("key", key).Msg("unable to find proper matcher") + } + } +} + +func makeDefaultDimensionDispatcher() dimensionDispatcher { + h := matcher.NewRadix(matcher.RadixCaseInsensitive()) + h.Register("Высота") + h.Register("Высота/*") + w := matcher.NewRadix(matcher.RadixCaseInsensitive()) + w.Register("Ширина") + w.Register("Ширина/*") + l := matcher.NewRadix(matcher.RadixCaseInsensitive()) + l.Register("Длина") + l.Register("Длина/*") + l.Register("Общ. длина") + + return dimensionDispatcher{ + heigth: h, + width: w, + length: l, + } +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 49bebfd..f0b8957 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -34,6 +34,8 @@ import ( "github.com/urfave/cli/v3" ) +type empty entity.Empty + func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer func() { @@ -506,14 +508,14 @@ func viewItemsUniqueParamsAction(ctx context.Context, c *cli.Command) error { return fmt.Errorf("getting repository: %w", err) } - knownParams := map[string]struct{}{} + knownParams := map[string]empty{} iter, err := repository.GoodsItem().ListIter(ctx, 1) if err != nil { return fmt.Errorf("getting list iter: %w", err) } for item := range iter { for k := range item.Parameters { - knownParams[k] = struct{}{} + knownParams[k] = empty{} } } @@ -595,8 +597,8 @@ func viewItemsParamsKnownValuesAction(ctx context.Context, c *cli.Command) error m.RegisterRegexp(regexp) } - requestedValues := make(map[string]map[string]struct{}, len(params)) - requestedValuesByPattern := make(map[string]map[string]struct{}, len(params)) + requestedValues := make(map[string]map[string]empty, len(params)) + requestedValuesByPattern := make(map[string]map[string]empty, len(params)) iter := getItemsIter(ctx, repository.GoodsItem()) for iter.Next() { item := iter.Get() @@ -608,16 +610,16 @@ func viewItemsParamsKnownValuesAction(ctx context.Context, c *cli.Command) error values, ok := requestedValues[k] if !ok { - values = make(map[string]struct{}) + values = make(map[string]empty) } - values[v] = struct{}{} + values[v] = empty{} requestedValues[k] = values values, ok = requestedValuesByPattern[matchedPattern] if !ok { - values = map[string]struct{}{} + values = map[string]empty{} } - values[v] = struct{}{} + values[v] = empty{} requestedValuesByPattern[matchedPattern] = values } } @@ -657,7 +659,7 @@ func viewItemsCountAction(ctx context.Context, c *cli.Command) error { filters := c.StringSlice("param-key-match") m := matcher.NewRadix() - patternMapped := make(map[string]struct{}, len(filters)) + patternMapped := make(map[string]empty, len(filters)) if len(filters) == 0 { m.Register("*") } else { @@ -665,7 +667,7 @@ func viewItemsCountAction(ctx context.Context, c *cli.Command) error { m.Register(f) } for _, pattern := range m.Patterns() { - patternMapped[pattern] = struct{}{} + patternMapped[pattern] = empty{} } } @@ -675,7 +677,7 @@ func viewItemsCountAction(ctx context.Context, c *cli.Command) error { return fmt.Errorf("getting items: %w", err) } for _, item := range items { - seenPatterns := map[string]struct{}{} + seenPatterns := map[string]empty{} for k := range item.Parameters { pattern := m.MatchByPattern(k) @@ -685,7 +687,7 @@ func viewItemsCountAction(ctx context.Context, c *cli.Command) error { if _, ok := seenPatterns[pattern]; ok { continue } - seenPatterns[pattern] = struct{}{} + seenPatterns[pattern] = empty{} } if len(seenPatterns) == len(patternMapped) { @@ -794,14 +796,14 @@ func importFromFileAction(ctx context.Context, c *cli.Command) error { failedToInsert int ) - seenCategories := make(map[string]struct{}) + seenCategories := make(map[string]empty) categories, err := r.Category().List(ctx) if err != nil { return fmt.Errorf("listing categories: %w", err) } for _, category := range categories { - seenCategories[category.Name] = struct{}{} + seenCategories[category.Name] = empty{} } bfile := bufio.NewReader(productsFile) @@ -841,7 +843,7 @@ func importFromFileAction(ctx context.Context, c *cli.Command) error { return fmt.Errorf("unable to create new category: %w", err) } log.Debug().Any("category", goodsItem.Type).Msg("inserted new category") - seenCategories[goodsItem.Type] = struct{}{} + seenCategories[goodsItem.Type] = empty{} } log.Debug().Int("count", len(goodsItems)).Int("failed", failedToInsert).Msg("preparing to upload") @@ -1137,16 +1139,16 @@ func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { goodsItems := make([]entity.GoodsItem, 0, batchSize) productIDs := make([]int, 0, batchSize) - knownCategories := make(map[string]struct{}) + knownCategories := make(map[string]empty) err = entity.IterWithErr(repository.Category().List(ctx)).Do(func(c entity.Category) error { - knownCategories[c.Name] = struct{}{} + knownCategories[c.Name] = empty{} return nil }) if err != nil { return fmt.Errorf("filling known categories: %w", err) } - itemsUpdated := make(map[string]struct{}, len(seenItems)) + itemsUpdated := make(map[string]empty, len(seenItems)) stats := struct { fetchedInfo int handledAll int @@ -1154,6 +1156,8 @@ func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { skippedItem int }{} + dimensionDispatcher := makeDefaultDimensionDispatcher() + startFrom := time.Now() for { select { @@ -1190,7 +1194,7 @@ func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { if time.Since(seenItem.CreatedAt) < time.Hour*24 { logger.Debug().Str("sku", item.SKU).Msg("skipping item because it's too fresh") stats.skippedItem++ - itemsUpdated[item.SKU] = struct{}{} + itemsUpdated[item.SKU] = empty{} continue } @@ -1213,7 +1217,11 @@ func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { continue } - itemsUpdated[goodsItem.Articul] = struct{}{} + for key, value := range goodsItem.Parameters { + dimensionDispatcher.dispatch(ctx, key, value, &goodsItem.Sizes) + } + + itemsUpdated[goodsItem.Articul] = empty{} stats.handledAll++ goodsItems = append(goodsItems, goodsItem) @@ -1235,7 +1243,7 @@ func parseEwayDumpAction(ctx context.Context, cmd *cli.Command) error { Int64("id", category.ID). Msg("created new category") - knownCategories[goodsItem.Type] = struct{}{} + knownCategories[goodsItem.Type] = empty{} } _, err = repository.GoodsItem().UpsertMany(ctx, goodsItems...) diff --git a/go.mod b/go.mod index 9d9d434..9aecb62 100644 --- a/go.mod +++ b/go.mod @@ -13,12 +13,14 @@ require ( github.com/rodaine/table v1.1.1 github.com/rs/zerolog v1.31.0 github.com/samber/do v1.6.0 + github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v3 v3.0.0-alpha8 ) require ( github.com/andybalholm/cascadia v1.3.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect @@ -26,12 +28,15 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 // indirect github.com/klauspost/compress v1.12.3 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect go.opencensus.io v0.22.5 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c087921..f6add52 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,7 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -53,6 +54,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -184,6 +189,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/encoding/fbs/Dimensions.go b/internal/encoding/fbs/Dimensions.go new file mode 100644 index 0000000..013000e --- /dev/null +++ b/internal/encoding/fbs/Dimensions.go @@ -0,0 +1,49 @@ +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + +package fbs + +import ( + flatbuffers "github.com/google/flatbuffers/go" +) + +type Dimensions struct { + _tab flatbuffers.Struct +} + +func (rcv *Dimensions) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *Dimensions) Table() flatbuffers.Table { + return rcv._tab.Table +} + +func (rcv *Dimensions) Width() float32 { + return rcv._tab.GetFloat32(rcv._tab.Pos + flatbuffers.UOffsetT(0)) +} +func (rcv *Dimensions) MutateWidth(n float32) bool { + return rcv._tab.MutateFloat32(rcv._tab.Pos+flatbuffers.UOffsetT(0), n) +} + +func (rcv *Dimensions) Height() float32 { + return rcv._tab.GetFloat32(rcv._tab.Pos + flatbuffers.UOffsetT(4)) +} +func (rcv *Dimensions) MutateHeight(n float32) bool { + return rcv._tab.MutateFloat32(rcv._tab.Pos+flatbuffers.UOffsetT(4), n) +} + +func (rcv *Dimensions) Length() float32 { + return rcv._tab.GetFloat32(rcv._tab.Pos + flatbuffers.UOffsetT(8)) +} +func (rcv *Dimensions) MutateLength(n float32) bool { + return rcv._tab.MutateFloat32(rcv._tab.Pos+flatbuffers.UOffsetT(8), n) +} + +func CreateDimensions(builder *flatbuffers.Builder, width float32, height float32, length float32) flatbuffers.UOffsetT { + builder.Prep(4, 12) + builder.PrependFloat32(length) + builder.PrependFloat32(height) + builder.PrependFloat32(width) + return builder.Offset() +} diff --git a/internal/encoding/fbs/GoodItem.go b/internal/encoding/fbs/GoodItem.go index 2682cb5..fd37bd3 100644 --- a/internal/encoding/fbs/GoodItem.go +++ b/internal/encoding/fbs/GoodItem.go @@ -169,8 +169,21 @@ func (rcv *GoodItem) MutateStock(n int16) bool { return rcv._tab.MutateInt16Slot(28, n) } -func (rcv *GoodItem) Parameters() []byte { +func (rcv *GoodItem) Sizes(obj *Dimensions) *Dimensions { o := flatbuffers.UOffsetT(rcv._tab.Offset(30)) + if o != 0 { + x := o + rcv._tab.Pos + if obj == nil { + obj = new(Dimensions) + } + obj.Init(rcv._tab.Bytes, x) + return obj + } + return nil +} + +func (rcv *GoodItem) Parameters() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(32)) if o != 0 { return rcv._tab.ByteVector(o + rcv._tab.Pos) } @@ -178,7 +191,7 @@ func (rcv *GoodItem) Parameters() []byte { } func (rcv *GoodItem) CreatedAt() int64 { - o := flatbuffers.UOffsetT(rcv._tab.Offset(32)) + o := flatbuffers.UOffsetT(rcv._tab.Offset(34)) if o != 0 { return rcv._tab.GetInt64(o + rcv._tab.Pos) } @@ -186,11 +199,11 @@ func (rcv *GoodItem) CreatedAt() int64 { } func (rcv *GoodItem) MutateCreatedAt(n int64) bool { - return rcv._tab.MutateInt64Slot(32, n) + return rcv._tab.MutateInt64Slot(34, n) } func GoodItemStart(builder *flatbuffers.Builder) { - builder.StartObject(15) + builder.StartObject(16) } func GoodItemAddSku(builder *flatbuffers.Builder, sku flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(sku), 0) @@ -231,11 +244,14 @@ func GoodItemAddCart(builder *flatbuffers.Builder, cart int64) { func GoodItemAddStock(builder *flatbuffers.Builder, stock int16) { builder.PrependInt16Slot(12, stock, 0) } +func GoodItemAddSizes(builder *flatbuffers.Builder, sizes flatbuffers.UOffsetT) { + builder.PrependStructSlot(13, flatbuffers.UOffsetT(sizes), 0) +} func GoodItemAddParameters(builder *flatbuffers.Builder, parameters flatbuffers.UOffsetT) { - builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(parameters), 0) + builder.PrependUOffsetTSlot(14, flatbuffers.UOffsetT(parameters), 0) } func GoodItemAddCreatedAt(builder *flatbuffers.Builder, createdAt int64) { - builder.PrependInt64Slot(14, createdAt, 0) + builder.PrependInt64Slot(15, createdAt, 0) } func GoodItemEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() diff --git a/internal/encoding/fbs/helpers.go b/internal/encoding/fbs/helpers.go index 0cdd110..d19e1df 100644 --- a/internal/encoding/fbs/helpers.go +++ b/internal/encoding/fbs/helpers.go @@ -82,6 +82,14 @@ func makeDomainGoodItem(builder *flatbuffers.Builder, in entity.GoodsItem) flatb producer := builder.CreateString(in.Producer) + var w, h, l float32 + if in.Sizes != (entity.GoodsItemSize{}) { + w = float32(in.Sizes.Width.AdjustTo(entity.DimensionKindCentimeter).Value) + h = float32(in.Sizes.Height.AdjustTo(entity.DimensionKindCentimeter).Value) + l = float32(in.Sizes.Length.AdjustTo(entity.DimensionKindCentimeter).Value) + } + sizes := CreateDimensions(builder, w, h, l) + GoodItemStart(builder) GoodItemAddSku(builder, sku) GoodItemAddPhoto(builder, photo) @@ -98,6 +106,7 @@ func makeDomainGoodItem(builder *flatbuffers.Builder, in entity.GoodsItem) flatb GoodItemAddTariff(builder, float32(in.TariffPrice)) GoodItemAddCart(builder, int64(in.Cart)) GoodItemAddStock(builder, int16(in.Stock)) + GoodItemAddSizes(builder, sizes) GoodItemAddParameters(builder, parameters) GoodItemAddCreatedAt(builder, in.CreatedAt.Unix()) @@ -136,6 +145,17 @@ func ParseGoodsItem(data []byte) (item entity.GoodsItem, err error) { } } + sizes := itemFBS.Sizes(nil) + w := float64(sizes.Width()) + h := float64(sizes.Height()) + l := float64(sizes.Length()) + + item.Sizes = entity.GoodsItemSize{ + Width: entity.NewMilimeterDimension(w), + Height: entity.NewMilimeterDimension(h), + Length: entity.NewMilimeterDimension(l), + } + createdAt := itemFBS.CreatedAt() if createdAt > 0 { item.CreatedAt = time.Unix(createdAt, 0) diff --git a/internal/entity/dimension.go b/internal/entity/dimension.go new file mode 100644 index 0000000..976d41b --- /dev/null +++ b/internal/entity/dimension.go @@ -0,0 +1,142 @@ +package entity + +import ( + "fmt" + "strconv" + "strings" +) + +var DefaultLocale = DimensionLocalRU + +type DimensionLocale uint8 + +const ( + DimensionLocalUnspecified DimensionLocale = iota + DimensionLocalRU + + dimensionLocalEnd +) + +type DimensionKind uint8 + +func (k DimensionKind) GetPos() float64 { + switch k { + case DimensionKindMilimeter: + return 1 + case DimensionKindCentimeter: + return 10 + case DimensionKindMeter: + return 1000 + default: + return 0 + } +} + +func (k DimensionKind) String() string { + m := getLocaleKindToStringMap()[DefaultLocale] + return m[k] +} + +const ( + DimensionKindUnspecified DimensionKind = iota + DimensionKindMilimeter + DimensionKindCentimeter + DimensionKindMeter +) + +type Dimension struct { + Value float64 + Kind DimensionKind +} + +func (d Dimension) MarshalText() ([]byte, error) { + value := strconv.FormatFloat(d.Value, 'f', 4, 64) + " " + d.Kind.String() + return []byte(value), nil +} + +func (d *Dimension) UnmarshalText(data []byte) (err error) { + *d, err = ParseDimention(string(data), DefaultLocale) + return err +} + +func (d Dimension) AdjustTo(kind DimensionKind) Dimension { + from := d.Kind.GetPos() + to := kind.GetPos() + + switch { + case from < to: + mult := to / from + return Dimension{ + Kind: kind, + Value: d.Value / float64(mult), + } + case from > to: + mult := from / to + return Dimension{ + Kind: kind, + Value: d.Value * float64(mult), + } + } + + return d +} + +func ParseDimention(value string, locale DimensionLocale) (Dimension, error) { + switch locale { + case DimensionLocalRU: + default: + return Dimension{}, SimpleError("unknown locale for parse") + } + + dimensionStrToKind := getLocaleToKindMap()[locale] + lastSpaceIdx := strings.LastIndex(value, " ") + if lastSpaceIdx == -1 { + return Dimension{}, SimpleError("expected 2 values after split for value " + value) + } + + var splitted [2]string + splitted[0] = strings.ReplaceAll(value[:lastSpaceIdx], " ", "") + splitted[1] = value[lastSpaceIdx+1:] + + var out Dimension + var ok bool + out.Kind, ok = dimensionStrToKind[splitted[1]] + if !ok { + return Dimension{}, SimpleError("dimension map not found for kind " + splitted[1]) + } + + var err error + out.Value, err = strconv.ParseFloat(splitted[0], 64) + if err != nil { + return Dimension{}, fmt.Errorf("parsing value: %w", err) + } + + return out, nil +} + +func NewMilimeterDimension(value float64) Dimension { + return Dimension{ + Value: value, + Kind: DimensionKindMilimeter, + } +} + +func getLocaleToKindMap() map[DimensionLocale]map[string]DimensionKind { + return map[DimensionLocale]map[string]DimensionKind{ + DimensionLocalRU: { + "мм": DimensionKindMilimeter, + "см": DimensionKindCentimeter, + "м": DimensionKindMeter, + }, + } +} + +func getLocaleKindToStringMap() map[DimensionLocale]map[DimensionKind]string { + return map[DimensionLocale]map[DimensionKind]string{ + DimensionLocalRU: { + DimensionKindMilimeter: "мм", + DimensionKindCentimeter: "см", + DimensionKindMeter: "м", + }, + } +} diff --git a/internal/entity/dimension_inner_test.go b/internal/entity/dimension_inner_test.go new file mode 100644 index 0000000..1808ae9 --- /dev/null +++ b/internal/entity/dimension_inner_test.go @@ -0,0 +1,31 @@ +package entity + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLocaleMap(t *testing.T) { + kindToStr := getLocaleKindToStringMap() + strToKind := getLocaleToKindMap() + + assert := assert.New(t) + for locale := DimensionLocalUnspecified + 1; locale < dimensionLocalEnd; locale++ { + localeKinds, ok := kindToStr[locale] + assert.True(ok) + localeStrs, ok := strToKind[locale] + assert.True(ok) + + assert.Equal(len(localeKinds), len(localeStrs)) + + for kindKey, kindValue := range localeKinds { + strKey := kindValue + strValue, ok := localeStrs[strKey] + assert.True(ok) + + assert.Equal(kindKey, strValue) + assert.Equal(strKey, kindValue) + } + } +} diff --git a/internal/entity/dimension_test.go b/internal/entity/dimension_test.go new file mode 100644 index 0000000..5dad974 --- /dev/null +++ b/internal/entity/dimension_test.go @@ -0,0 +1,72 @@ +package entity_test + +import ( + "errors" + "testing" + + "git.loyso.art/frx/eway/internal/entity" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDimension_AdjustTo(t *testing.T) { + // Test adjusting from smaller dimension to larger one + d := entity.Dimension{Value: 5.0, Kind: entity.DimensionKindCentimeter} + expected := entity.Dimension{Value: 0.05, Kind: entity.DimensionKindMeter} + actual := d.AdjustTo(entity.DimensionKindMeter) + + assert.EqualValues(t, expected.Value, actual.Value) + assert.Equal(t, expected.Kind, actual.Kind) + + // Test adjusting from larger dimension to smaller one + d = entity.Dimension{Value: 0.05, Kind: entity.DimensionKindMeter} + expected = entity.Dimension{Value: 50.0, Kind: entity.DimensionKindMilimeter} + actual = d.AdjustTo(entity.DimensionKindMilimeter) + + assert.EqualValues(t, expected.Value, actual.Value) + assert.Equal(t, expected.Kind, actual.Kind) +} + +func TestParseDimension_Success(t *testing.T) { + // Test parsing a valid dimension string with RU locale + input := "10 см" + expected := entity.Dimension{Value: 10.0, Kind: entity.DimensionKindCentimeter} + + actual, err := entity.ParseDimention(input, entity.DimensionLocalRU) + require.NoError(t, err) + + assert.EqualValues(t, expected.Value, actual.Value) + assert.Equal(t, expected.Kind, actual.Kind) +} + +func TestParseDimensionComplex_Success(t *testing.T) { + // Test parsing a valid dimension string with RU locale + input := "10 256.20 см" + expected := entity.Dimension{Value: 10256.20, Kind: entity.DimensionKindCentimeter} + + actual, err := entity.ParseDimention(input, entity.DimensionLocalRU) + require.NoError(t, err) + + assert.EqualValues(t, expected.Value, actual.Value) + assert.Equal(t, expected.Kind, actual.Kind) +} + +func TestParseDimension_InvalidInputFormat(t *testing.T) { + // Test parsing an invalid dimension string with RU locale + input := "invalid value 2" + expectedErr := errors.New("expected 2 values after split for value invalid value 2") + + _, err := entity.ParseDimention(input, entity.DimensionLocalRU) + assert.Error(t, err) + assert.EqualError(t, err, expectedErr.Error()) +} + +func TestParseDimension_InvalidLocale(t *testing.T) { + // Test parsing a dimension string with an unsupported locale + input := "10 мм" + expectedErr := errors.New("unknown locale for parse") + + _, err := entity.ParseDimention(input, 3) // An invalid locale value is used here for demonstration purposes + assert.EqualError(t, err, expectedErr.Error()) +} diff --git a/internal/entity/empty.go b/internal/entity/empty.go new file mode 100644 index 0000000..e1f67a2 --- /dev/null +++ b/internal/entity/empty.go @@ -0,0 +1,3 @@ +package entity + +type Empty struct{} diff --git a/internal/entity/gooditem.go b/internal/entity/gooditem.go index ef7f5e7..31a0345 100644 --- a/internal/entity/gooditem.go +++ b/internal/entity/gooditem.go @@ -8,6 +8,26 @@ import ( "unicode" ) +type GoodsItemSize struct { + Width Dimension + Height Dimension + Length Dimension +} + +func (s GoodsItemSize) GetSum(kind DimensionKind) float64 { + var value float64 + sum := func(ds ...Dimension) { + for _, d := range ds { + value += d.AdjustTo(kind).Value + + } + } + + sum(s.Height, s.Length, s.Length) + + return value +} + type GoodsItem struct { Articul string `json:"sku"` PhotoURLs []string `json:"photo"` @@ -22,6 +42,7 @@ type GoodsItem struct { TariffPrice float64 `json:"tariff_price"` Cart int64 `json:"cart"` Stock int `json:"stock"` + Sizes GoodsItemSize `json:"sizes"` Parameters map[string]string `json:"parameters"` CreatedAt time.Time `json:"created_at"` }