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 a1a420c..ca5c472 100644 --- a/box.go +++ b/box.go @@ -1,13 +1,19 @@ package boxpacker3 +import ( + "golang.org/x/exp/slices" +) + type Box struct { - id string - width float64 - height float64 - depth float64 - maxWeight float64 - volume float64 - items []*Item + id string + width float64 + height float64 + depth float64 + maxWeight float64 + volume float64 + items []*Item + + maxLength float64 itemsVolume float64 itemsWeight float64 } @@ -19,7 +25,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 +40,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, } @@ -80,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 } @@ -88,30 +95,26 @@ 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,12 +123,12 @@ 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() { - 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..cc0e09b 100644 --- a/copyptr.go +++ b/copyptr.go @@ -8,8 +8,10 @@ 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 0819a4a..49ecf53 100644 --- a/item.go +++ b/item.go @@ -2,15 +2,19 @@ package boxpacker3 import ( "math" + + "golang.org/x/exp/slices" ) 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 } @@ -32,12 +36,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}), } } @@ -69,24 +74,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 2177119..6d10d75 100644 --- a/packer.go +++ b/packer.go @@ -25,20 +25,18 @@ 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), } - 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,17 +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)...) - - break + if b.volume >= volume && b.maxWeight >= weight && b.maxLength >= maxLength { + return append(boxSlice{b}, slices.Delete(boxes, i, i)...) } } @@ -67,26 +69,36 @@ 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], pv) { + 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 +117,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 +147,7 @@ func (p *Packer) packToBox(boxes []*Box, b *Box, items []*Item) []*Item { } } - fitted = iter == total + fitted = itemsFit == total } if !fitted { @@ -137,10 +155,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 +162,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..762bb20 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,32 @@ 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) + } +} + +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()) } }