From 157df0683745a6aba93bec1d7bd06e03531d7aaf Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Mon, 7 Aug 2023 18:49:02 +0300 Subject: [PATCH 1/4] packer.go optimize --- box.go | 21 +++++------ item.go | 15 ++++---- packer.go | 93 +++++++++++++++++++++++------------------------- packer_test.go | 96 +++++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 148 insertions(+), 77 deletions(-) diff --git a/box.go b/box.go index a1a420c..85344e8 100644 --- a/box.go +++ b/box.go @@ -1,11 +1,14 @@ package boxpacker3 +import "golang.org/x/exp/slices" + type Box struct { id string width float64 height float64 depth float64 maxWeight float64 + maxLength float64 volume float64 items []*Item itemsVolume float64 @@ -19,7 +22,7 @@ func (bs boxSlice) Len() int { } func (bs boxSlice) Less(i, j int) bool { - return bs[i].GetVolume() < bs[j].GetVolume() + return bs[i].volume < bs[j].volume } func (bs boxSlice) Swap(i, j int) { @@ -34,6 +37,7 @@ func NewBox(id string, w, h, d, mw float64) *Box { height: h, depth: d, maxWeight: mw, + maxLength: slices.Max([]float64{w, h, d}), volume: w * h * d, items: nil, } @@ -88,30 +92,27 @@ func (b *Box) PutItem(item *Item, p Pivot) bool { for _, ib := range b.items { if ib.Intersect(item) { - fit = false - - break + return false } } if fit { b.insert(item) - return fit + break } - break } return fit } func (b *Box) canTryToPlace(item *Item) bool { - if b.itemsVolume+item.GetVolume() > b.GetVolume() { + if b.itemsVolume+item.volume > b.volume { return false } - if b.itemsWeight+item.GetWeight() > b.GetMaxWeight() { + if b.itemsWeight+item.weight > b.maxWeight { return false } @@ -120,8 +121,8 @@ func (b *Box) canTryToPlace(item *Item) bool { func (b *Box) insert(item *Item) { b.items = append(b.items, item) - b.itemsVolume += item.GetVolume() - b.itemsWeight += item.GetWeight() + b.itemsVolume += item.volume + b.itemsWeight += item.weight } func (b *Box) purge() { diff --git a/item.go b/item.go index 0819a4a..59e7a47 100644 --- a/item.go +++ b/item.go @@ -1,6 +1,7 @@ package boxpacker3 import ( + "golang.org/x/exp/slices" "math" ) @@ -11,6 +12,7 @@ type Item struct { depth float64 weight float64 volume float64 + maxLength float64 rotationType RotationType position Pivot } @@ -32,12 +34,13 @@ func (it itemSlice) Swap(i, j int) { func NewItem(id string, w, h, d, wg float64) *Item { //nolint:exhaustruct return &Item{ - id: id, - width: w, - height: h, - depth: d, - weight: wg, - volume: w * h * d, + id: id, + width: w, + height: h, + depth: d, + weight: wg, + volume: w * h * d, + maxLength: slices.Max([]float64{w, h, d}), } } diff --git a/packer.go b/packer.go index 2177119..cd35e13 100644 --- a/packer.go +++ b/packer.go @@ -29,16 +29,14 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) (*Result, error) { Boxes: p.preferredSort(boxes, items), } - for i := 0; len(items) > 0; i++ { - box := p.findRightBox(result.Boxes, items[0]) - if box == nil { - result.UnfitItems = append(result.UnfitItems, items[0]) - items = items[1:] - - continue + for _, box := range result.Boxes { + if items = p.packToBox(box, items); len(items) == 0 { + break } + } - items = p.packToBox(result.Boxes, box, items) + if len(items) > 0 { + result.UnfitItems = append(result.UnfitItems, items...) } return result, nil @@ -47,15 +45,21 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) (*Result, error) { func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { volume := 0. weight := 0. + maxLength := 0. for _, item := range items { volume += item.GetVolume() weight += item.GetWeight() + + // optimize + if maxLength < item.maxLength { + maxLength = item.maxLength + } } for i, b := range boxes { - if b.GetVolume() >= volume && b.GetMaxWeight() >= weight { - boxes = append(boxSlice{b}, slices.Delete(boxes, i, i+1)...) + if b.volume >= volume && b.maxWeight >= weight && b.maxLength >= maxLength { + boxes = append(boxSlice{b}, slices.Delete(boxes, i, len(boxes))...) break } @@ -67,26 +71,35 @@ func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { // packToBox Packs goods in a box b. Returns unpackaged goods. // //nolint:cyclop,gocognit,funlen -func (p *Packer) packToBox(boxes []*Box, b *Box, items []*Item) []*Item { +func (p *Packer) packToBox(b *Box, items []*Item) []*Item { var fitted bool unpacked := make([]*Item, 0, len(items)) pv := Pivot{} + if b.items == nil && b.PutItem(items[0], Pivot{}) { + items = items[1:] + } + // Packing unpackaged goods. for _, i := range items { fitted = false // Trying anchor points for the box that don't intersect with existing items in the box. - for pt := WidthAxis; pt <= DepthAxis && !fitted; pt++ { - for _, ib := range b.items { + for j := range b.items { + dimension := b.items[j].GetDimension() + for pt := WidthAxis; pt <= DepthAxis && !fitted; pt++ { + pv[WidthAxis] = b.items[j].position[WidthAxis] + pv[HeightAxis] = b.items[j].position[HeightAxis] + pv[DepthAxis] = b.items[j].position[DepthAxis] + switch pt { case WidthAxis: - pv = Pivot{ib.position[WidthAxis] + ib.GetWidth(), ib.position[HeightAxis], ib.position[DepthAxis]} + pv[WidthAxis] += +dimension[WidthAxis] case HeightAxis: - pv = Pivot{ib.position[WidthAxis], ib.position[HeightAxis] + ib.GetHeight(), ib.position[DepthAxis]} + pv[HeightAxis] += dimension[HeightAxis] case DepthAxis: - pv = Pivot{ib.position[WidthAxis], ib.position[HeightAxis], ib.position[DepthAxis] + ib.GetDepth()} + pv[DepthAxis] += dimension[DepthAxis] } if b.PutItem(i, pv) { @@ -105,23 +118,29 @@ func (p *Packer) packToBox(boxes []*Box, b *Box, items []*Item) []*Item { if b.PutItem(i, Pivot{}) { total := len(copyItems) - iter := 0 + itemsFit := 0 - for k := 0; k < total && iter < total; k++ { + for k := 0; k < total && itemsFit < total; k++ { // Trying anchor points for the box that don't intersect with existing items in the box. - for pt := WidthAxis; pt <= DepthAxis && iter < total; pt++ { - for _, ib := range b.items { + for j := len(b.items) - 1; j >= 0; j-- { + dimension := b.items[j].GetDimension() + + for pt := WidthAxis; pt <= DepthAxis && k < total; pt++ { + pv[WidthAxis] = b.items[j].position[WidthAxis] + pv[HeightAxis] = b.items[j].position[HeightAxis] + pv[DepthAxis] = b.items[j].position[DepthAxis] + switch pt { case WidthAxis: - pv = Pivot{ib.position[WidthAxis] + ib.GetWidth(), ib.position[HeightAxis], ib.position[DepthAxis]} + pv[WidthAxis] += +dimension[WidthAxis] case HeightAxis: - pv = Pivot{ib.position[WidthAxis], ib.position[HeightAxis] + ib.GetHeight(), ib.position[DepthAxis]} + pv[HeightAxis] += dimension[HeightAxis] case DepthAxis: - pv = Pivot{ib.position[WidthAxis], ib.position[HeightAxis], ib.position[DepthAxis] + ib.GetDepth()} + pv[DepthAxis] += dimension[DepthAxis] } if b.PutItem(copyItems[k], pv) { - iter++ + itemsFit++ break } @@ -129,7 +148,7 @@ func (p *Packer) packToBox(boxes []*Box, b *Box, items []*Item) []*Item { } } - fitted = iter == total + fitted = itemsFit == total } if !fitted { @@ -137,10 +156,6 @@ func (p *Packer) packToBox(boxes []*Box, b *Box, items []*Item) []*Item { } } - for lb := p.findBiggerBox(boxes, b); lb != nil && !fitted; lb = p.findBiggerBox(boxes, lb) { - fitted = len(p.packToBox(boxes, lb, itemSlice{i})) == 0 - } - if !fitted { unpacked = append(unpacked, i) } @@ -148,23 +163,3 @@ func (p *Packer) packToBox(boxes []*Box, b *Box, items []*Item) []*Item { return unpacked } - -func (p *Packer) findBiggerBox(boxes []*Box, box *Box) *Box { - for _, b := range boxes { - if b.volume > box.volume { - return b - } - } - - return nil -} - -func (p *Packer) findRightBox(boxes []*Box, item *Item) *Box { - for _, b := range boxes { - if b.volume >= item.volume { - return b - } - } - - return nil -} diff --git a/packer_test.go b/packer_test.go index 42eb208..440e991 100644 --- a/packer_test.go +++ b/packer_test.go @@ -1,4 +1,3 @@ -//nolint:dupl package boxpacker3_test import ( @@ -52,16 +51,6 @@ const ( BoxTypeNotStd6 = "981ffb30-a7b9-4d9e-820e-04de2145763e" ) -type PackerSuit struct { - suite.Suite -} - -func TestBoxPackerSuite(t *testing.T) { - t.Parallel() - - suite.Run(t, new(PackerSuit)) -} - func NewDefaultBoxList() []*boxpacker3.Box { return []*boxpacker3.Box{ boxpacker3.NewBox(BoxTypeF, 220, 185, 50, 20000), // 0 @@ -80,6 +69,16 @@ func NewDefaultBoxList() []*boxpacker3.Box { } } +type PackerSuit struct { + suite.Suite +} + +func TestBoxPackerSuite(t *testing.T) { + t.Parallel() + + suite.Run(t, new(PackerSuit)) +} + func (s *PackerSuit) TestMinBox() { t := s.T() t.Parallel() @@ -168,6 +167,79 @@ func (s *PackerSuit) TestStd() { } } +func (s *PackerSuit) TestBoxTypeF() { + t := s.T() + t.Parallel() + + packer := boxpacker3.NewPacker() + boxes := NewDefaultBoxList() + + items := []*boxpacker3.Item{ + // 5 + boxpacker3.NewItem(uuid.New().String(), 100, 100, 5, 2500), + boxpacker3.NewItem(uuid.New().String(), 100, 5, 100, 2500), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), + + // 35 + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2500), + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2500), + } + + packResult, err := packer.Pack(boxes, items) + require.NoError(t, err) + require.NotNil(t, packResult) + + checks := map[string]int{ + BoxTypeF: 8, + } + + require.Len(t, packResult.UnfitItems, 0) + + for i := 0; i < len(packResult.Boxes); i++ { + require.Len(t, packResult.Boxes[i].GetItems(), checks[packResult.Boxes[i].GetID()], packResult.Boxes[i].GetID()) + } +} + +func (s *PackerSuit) TestBoxTypeF_Weight() { + t := s.T() + t.Parallel() + + packer := boxpacker3.NewPacker() + boxes := NewDefaultBoxList() + + items := []*boxpacker3.Item{ + // 5 + boxpacker3.NewItem(uuid.New().String(), 100, 100, 5, 2690), + boxpacker3.NewItem(uuid.New().String(), 100, 5, 100, 2690), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), + + // 35 + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2690), + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2690), // maxWeight > 20_000 + } + + packResult, err := packer.Pack(boxes, items) + require.NoError(t, err) + require.NotNil(t, packResult) + + checks := map[string]int{ + BoxTypeF: 7, + BoxTypeE: 1, + } + + require.Len(t, packResult.UnfitItems, 0) + + for i := 0; i < len(packResult.Boxes); i++ { + require.Len(t, packResult.Boxes[i].GetItems(), checks[packResult.Boxes[i].GetID()], packResult.Boxes[i].GetID()) + } +} + func (s *PackerSuit) TestPacker_AllBoxes() { t := s.T() t.Parallel() @@ -205,7 +277,7 @@ func (s *PackerSuit) TestPacker_AllBoxes() { require.Len(t, packResult.UnfitItems, 0) for i := 0; i < len(packResult.Boxes); i++ { - require.Len(t, packResult.Boxes[i].GetItems(), 1, packResult.Boxes[i].GetID()) + require.Len(t, packResult.Boxes[i].GetItems(), 1) } } From 2a1935d98e4300b863ebb71a086b527458aa8bf5 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Mon, 7 Aug 2023 19:23:18 +0300 Subject: [PATCH 2/4] packer.go optimize --- box.go | 23 +++++++++++++---------- copyptr.go | 3 ++- item.go | 32 ++++++++++++++++---------------- packer.go | 8 +++----- packer_test.go | 25 +++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 32 deletions(-) diff --git a/box.go b/box.go index 85344e8..24c6715 100644 --- a/box.go +++ b/box.go @@ -1,16 +1,19 @@ package boxpacker3 -import "golang.org/x/exp/slices" +import ( + "golang.org/x/exp/slices" +) type Box struct { - id string - width float64 - height float64 - depth float64 - maxWeight float64 + id string + width float64 + height float64 + depth float64 + maxWeight float64 + volume float64 + items []*Item + maxLength float64 - volume float64 - items []*Item itemsVolume float64 itemsWeight float64 } @@ -84,7 +87,7 @@ func (b *Box) PutItem(item *Item, p Pivot) bool { item.rotationType = rt d := item.GetDimension() - if b.GetWidth() < p[WidthAxis]+d[WidthAxis] || b.GetHeight() < p[HeightAxis]+d[HeightAxis] || b.GetDepth() < p[DepthAxis]+d[DepthAxis] { + if b.width < p[WidthAxis]+d[WidthAxis] || b.height < p[HeightAxis]+d[HeightAxis] || b.depth < p[DepthAxis]+d[DepthAxis] { continue } @@ -126,7 +129,7 @@ func (b *Box) insert(item *Item) { } func (b *Box) purge() { - b.items = []*Item{} + b.items = b.items[:0] // keep allocated memory b.itemsVolume = 0 b.itemsWeight = 0 } diff --git a/copyptr.go b/copyptr.go index 3c3408a..6b043a4 100644 --- a/copyptr.go +++ b/copyptr.go @@ -9,7 +9,8 @@ func copyPtr[T any](original *T) *T { func copySlicePtr[T any](data []*T) []*T { result := make([]*T, len(data)) for i := range data { - result[i] = copyPtr(data[i]) + val := *data[i] + result[i] = &val } return result diff --git a/item.go b/item.go index 59e7a47..94c5e91 100644 --- a/item.go +++ b/item.go @@ -6,12 +6,13 @@ import ( ) type Item struct { - id string - width float64 - height float64 - depth float64 - weight float64 - volume float64 + id string + width float64 + height float64 + depth float64 + weight float64 + volume float64 + maxLength float64 rotationType RotationType position Pivot @@ -72,24 +73,23 @@ func (i *Item) GetPosition() Pivot { return i.position } -//nolint:nonamedreturns -func (i *Item) GetDimension() (d Dimension) { +func (i *Item) GetDimension() Dimension { switch i.rotationType { case RotationTypeWhd: - d = Dimension{i.GetWidth(), i.GetHeight(), i.GetDepth()} + return Dimension{i.GetWidth(), i.GetHeight(), i.GetDepth()} case RotationTypeHwd: - d = Dimension{i.GetHeight(), i.GetWidth(), i.GetDepth()} + return Dimension{i.GetHeight(), i.GetWidth(), i.GetDepth()} case RotationTypeHdw: - d = Dimension{i.GetHeight(), i.GetDepth(), i.GetWidth()} + return Dimension{i.GetHeight(), i.GetDepth(), i.GetWidth()} case RotationTypeDhw: - d = Dimension{i.GetDepth(), i.GetHeight(), i.GetWidth()} + return Dimension{i.GetDepth(), i.GetHeight(), i.GetWidth()} case RotationTypeDwh: - d = Dimension{i.GetDepth(), i.GetWidth(), i.GetHeight()} + return Dimension{i.GetDepth(), i.GetWidth(), i.GetHeight()} case RotationTypeWdh: - d = Dimension{i.GetWidth(), i.GetDepth(), i.GetHeight()} + return Dimension{i.GetWidth(), i.GetDepth(), i.GetHeight()} + default: // RotationTypeWhd + return Dimension{i.GetWidth(), i.GetHeight(), i.GetDepth()} } - - return } // Intersect Tests for intersections between the i element and the it element. diff --git a/packer.go b/packer.go index cd35e13..240bc06 100644 --- a/packer.go +++ b/packer.go @@ -25,7 +25,7 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) (*Result, error) { sort.Sort(items) result := &Result{ - UnfitItems: make(itemSlice, 0, len(items)), + UnfitItems: nil, Boxes: p.preferredSort(boxes, items), } @@ -59,9 +59,7 @@ func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { for i, b := range boxes { if b.volume >= volume && b.maxWeight >= weight && b.maxLength >= maxLength { - boxes = append(boxSlice{b}, slices.Delete(boxes, i, len(boxes))...) - - break + return append(boxSlice{b}, slices.Delete(boxes, i, i)...) } } @@ -77,7 +75,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { unpacked := make([]*Item, 0, len(items)) pv := Pivot{} - if b.items == nil && b.PutItem(items[0], Pivot{}) { + if b.items == nil && b.PutItem(items[0], pv) { items = items[1:] } diff --git a/packer_test.go b/packer_test.go index 440e991..762bb20 100644 --- a/packer_test.go +++ b/packer_test.go @@ -281,6 +281,31 @@ func (s *PackerSuit) TestPacker_AllBoxes() { } } +func (s *PackerSuit) TestPacker_UnfitItems() { + t := s.T() + t.Parallel() + + packer := boxpacker3.NewPacker() + boxes := NewDefaultBoxList() + + items := []*boxpacker3.Item{ + boxpacker3.NewItem(uuid.New().String(), 3001, 3000, 3000, 20000), + boxpacker3.NewItem(uuid.New().String(), 3000, 3001, 3000, 20000), + boxpacker3.NewItem(uuid.New().String(), 3000, 3000, 3001, 20000), + boxpacker3.NewItem(uuid.New().String(), 3000, 3000, 3000, 20001), + } + + packResult, err := packer.Pack(boxes, items) + require.NoError(t, err) + require.NotNil(t, packResult) + + require.Len(t, packResult.UnfitItems, 4) + + for i := 0; i < len(packResult.Boxes); i++ { + require.Len(t, packResult.Boxes[i].GetItems(), 0, packResult.Boxes[i].GetID()) + } +} + func (s *PackerSuit) TestPacker_MinAndStd() { t := s.T() t.Parallel() From f94939f51ba3692bfe834b7ccbd592fc7109640d Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Mon, 7 Aug 2023 19:29:57 +0300 Subject: [PATCH 3/4] lint fix --- item.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/item.go b/item.go index 94c5e91..49ecf53 100644 --- a/item.go +++ b/item.go @@ -1,8 +1,9 @@ package boxpacker3 import ( - "golang.org/x/exp/slices" "math" + + "golang.org/x/exp/slices" ) type Item struct { From 7b0d233772f18e62de281d844e190e7b98dd9b44 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Mon, 7 Aug 2023 19:34:12 +0300 Subject: [PATCH 4/4] lint fix --- .golangci.yml | 5 +++++ box.go | 1 - copyptr.go | 1 + packer.go | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 25da098..f02d500 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -30,3 +30,8 @@ linters-settings: allow: - $gostd - github.com +issues: + exclude-rules: + - path: (.+)_test.go + linters: + - dupl diff --git a/box.go b/box.go index 24c6715..ca5c472 100644 --- a/box.go +++ b/box.go @@ -104,7 +104,6 @@ func (b *Box) PutItem(item *Item, p Pivot) bool { break } - } return fit diff --git a/copyptr.go b/copyptr.go index 6b043a4..cc0e09b 100644 --- a/copyptr.go +++ b/copyptr.go @@ -8,6 +8,7 @@ func copyPtr[T any](original *T) *T { func copySlicePtr[T any](data []*T) []*T { result := make([]*T, len(data)) + for i := range data { val := *data[i] result[i] = &val diff --git a/packer.go b/packer.go index 240bc06..6d10d75 100644 --- a/packer.go +++ b/packer.go @@ -86,6 +86,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // Trying anchor points for the box that don't intersect with existing items in the box. for j := range b.items { dimension := b.items[j].GetDimension() + for pt := WidthAxis; pt <= DepthAxis && !fitted; pt++ { pv[WidthAxis] = b.items[j].position[WidthAxis] pv[HeightAxis] = b.items[j].position[HeightAxis]