From 85bbee888fd6b5f27d3773d0663f67a89ce76c5e Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 18:47:52 +0300 Subject: [PATCH 1/8] docs --- item.go | 21 +++++----- packer.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 26 deletions(-) diff --git a/item.go b/item.go index 9d34831..c0fcf89 100644 --- a/item.go +++ b/item.go @@ -2,8 +2,7 @@ package boxpacker3 import ( "math" - - "golang.org/x/exp/slices" + "slices" ) type Item struct { @@ -98,17 +97,17 @@ func (i *Item) Intersect(it *Item) bool { d1 := i.GetDimension() d2 := it.GetDimension() - return rectIntersect(d1, d2, i, it, WidthAxis, HeightAxis) && - rectIntersect(d1, d2, i, it, HeightAxis, DepthAxis) && - rectIntersect(d1, d2, i, it, WidthAxis, DepthAxis) + return i.intersect(d1, d2, it, WidthAxis, HeightAxis) && + i.intersect(d1, d2, it, HeightAxis, DepthAxis) && + i.intersect(d1, d2, it, WidthAxis, DepthAxis) } -// rectIntersect Checks if two rectangles intersect from the x and y axes of elements i1 and i2. -func rectIntersect(d1, d2 Dimension, i1, i2 *Item, x, y Axis) bool { - cx1 := i1.position[x] + d1[x]/2 //nolint:mnd - cy1 := i1.position[y] + d1[y]/2 //nolint:mnd - cx2 := i2.position[x] + d2[x]/2 //nolint:mnd - cy2 := i2.position[y] + d2[y]/2 //nolint:mnd +// intersect Checks if two rectangles intersect from the x and y axes of elements i1 and i2. +func (i *Item) intersect(d1, d2 Dimension, it *Item, x, y Axis) bool { + cx1 := i.position[x] + d1[x]/2 //nolint:mnd + cy1 := i.position[y] + d1[y]/2 //nolint:mnd + cx2 := it.position[x] + d2[x]/2 //nolint:mnd + cy2 := it.position[y] + d2[y]/2 //nolint:mnd ix := math.Max(cx1, cx2) - math.Min(cx1, cx2) iy := math.Max(cy1, cy2) - math.Min(cy1, cy2) diff --git a/packer.go b/packer.go index e59ae24..549c758 100644 --- a/packer.go +++ b/packer.go @@ -5,35 +5,77 @@ import ( "sort" ) +// Packer is a struct that packs items into boxes. +// +// It sorts input boxes and items by volume and weight. +// It then selects the box with the largest volume and weight +// that can accommodate the items. If there are still items left +// after packing the boxes, it sets them as unfit items. type Packer struct{} +// Result represents the result of packing items into boxes. +// +// It is a struct that contains two slices: +// - UnfitItems: a list of items that didn't fit into boxes. +// - Boxes: a list of boxes with items. type Result struct { + // UnfitItems is a list of items that didn't fit into boxes. UnfitItems itemSlice - Boxes boxSlice + + // Boxes is a list of boxes with items. + Boxes boxSlice } +// NewPacker creates a new instance of Packer. +// +// Returns: +// - a pointer to a Packer struct. func NewPacker() *Packer { return &Packer{} } +// Pack packs items into boxes. +// +// This function sorts input boxes and items by volume and weight. +// It selects the box with the largest volume and weight that +// can accommodate the items. If there are still items left +// after packing the boxes, they will be set as unfit items. +// +// Parameters: +// - inputBoxes: a list of boxes. +// - inputItems: a list of items. +// +// Returns: +// - a Result struct that contains two slices: +// - Boxes: a list of boxes with items. +// - UnfitItems: a list of items that didn't fit into boxes. func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) *Result { + // Copy input slices to avoid modifying them. boxes := boxSlice(CopySlicePtr(inputBoxes)) items := itemSlice(CopySlicePtr(inputItems)) + // Sort boxes and items in ascending order. sort.Sort(boxes) sort.Sort(items) + // Create a new Result struct with empty slices. result := &Result{ UnfitItems: nil, Boxes: p.preferredSort(boxes, items), } + // Pack items into boxes. for _, box := range result.Boxes { - if items = p.packToBox(box, items); len(items) == 0 { + // If there are no items left, exit the loop. + if len(items) == 0 { break } + + // Pack items into the box. + items = p.packToBox(box, items) } + // If there are still items left, set them as unfit items. if len(items) > 0 { result.UnfitItems = append(result.UnfitItems, items...) } @@ -41,6 +83,19 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) *Result { return result } +// preferredSort selects the box with the largest volume and weight +// that can accommodate the items. +// +// Parameters: +// - boxes: a slice of boxes. +// - items: a slice of items. +// +// Returns: +// - a slice of boxes sorted by volume, weight and maxLength. +// The first box in the slice is the preferred box. +// The remaining boxes are sorted after the preferred box. +// If there is no box that can accommodate the items, the original +// slice of boxes is returned. func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { volume := 0. weight := 0. @@ -67,31 +122,46 @@ func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { // packToBox Packs goods in a box b. Returns unpackaged goods. // -//nolint:cyclop,gocognit,funlen +// It attempts to pack items into the box. If the box is not big enough +// to accommodate all the items, it tries to pack the items again +// into the box by rotating the box and items. +// If the box is still not big enough, it adds the items to the unpacked slice. +// +// Parameters: +// - b: the box to pack items into. +// - items: the items to pack into the box. +// +// Returns: +// - a slice of items that did not fit into the box. +// +//nolint:funlen,gocognit,cyclop func (p *Packer) packToBox(b *Box, items []*Item) []*Item { var fitted bool - cntItems := len(items) - unpacked := make([]*Item, 0, cntItems) + unpacked := make([]*Item, 0, len(items)) pv := Pivot{} - if b.items == nil && cntItems > 0 && b.PutItem(items[0], pv) { + // if box is empty, first item is put in box + if b.items == nil && len(items) > 0 && b.PutItem(items[0], pv) { items = items[1:] } - // Packing unpackaged goods. + // try to pack all items into the box for _, i := range items { fitted = false - // Trying anchor points for the box that don't intersect with existing items in the box. + // for each item already in the box for j := range b.items { dimension := b.items[j].GetDimension() - for pt := WidthAxis; pt <= DepthAxis && !fitted; pt++ { + // for each axis + for _, pt := range []Axis{WidthAxis, HeightAxis, DepthAxis} { + // calculate pivot position pv[WidthAxis] = b.items[j].position[WidthAxis] pv[HeightAxis] = b.items[j].position[HeightAxis] pv[DepthAxis] = b.items[j].position[DepthAxis] + // add item dimension to pivot position switch pt { case WidthAxis: pv[WidthAxis] += dimension[WidthAxis] @@ -101,30 +171,43 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { pv[DepthAxis] += dimension[DepthAxis] } - fitted = b.PutItem(i, pv) + // if item can be put in the box + if b.PutItem(i, pv) { + fitted = true + + break + } } } + // if item can not be put in the box if !fitted { + // make a backup of box backup := CopyPtr(b) copyItems := CopySlicePtr(b.items) + // clean box b.purge() + // try to put item into the box if b.PutItem(i, Pivot{}) { - total := len(copyItems) + // count of items fit in the box itemsFit := 0 - 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 each item that was in the box + for k := 0; k < len(copyItems) && itemsFit < len(copyItems); k++ { + // try to put item in the box for j := len(b.items) - 1; j >= 0; j-- { dimension := b.items[j].GetDimension() - for pt := WidthAxis; pt <= DepthAxis && k < total; pt++ { + // for each axis + for _, pt := range []Axis{WidthAxis, HeightAxis, DepthAxis} { + // calculate pivot position pv[WidthAxis] = b.items[j].position[WidthAxis] pv[HeightAxis] = b.items[j].position[HeightAxis] pv[DepthAxis] = b.items[j].position[DepthAxis] + // add item dimension to pivot position switch pt { case WidthAxis: pv[WidthAxis] += dimension[WidthAxis] @@ -134,6 +217,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { pv[DepthAxis] += dimension[DepthAxis] } + // if item can be put in the box if b.PutItem(copyItems[k], pv) { itemsFit++ @@ -143,18 +227,24 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { } } - fitted = itemsFit == total + // if all items that were in the box now fit in the box + fitted = itemsFit == len(copyItems) } + // if not all items that were in the box now fit in the box if !fitted { + // restore box from backup *b = *backup } } + // if item can not be put in the box if !fitted { + // add item to unpacked slice unpacked = append(unpacked, i) } } + // return unpacked slice return unpacked } From c82ddbc751571504b9dfa620170679f1cab2526c Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 21:03:09 +0300 Subject: [PATCH 2/8] update docs --- .github/dependabot.yml | 5 ++ .golangci.yml | 1 + bench_test.go | 3 +- box.go | 158 +++++++++++++++++++++++++++++++++++------ consts.go | 20 ++++-- copyptr.go | 12 ++++ go.mod | 1 - go.sum | 2 - item.go | 69 ++++++++++++++---- packer.go | 71 ++++++++++++++---- packer_test.go | 38 ++++++---- 11 files changed, 308 insertions(+), 72 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 03fa963..5979871 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,8 @@ updates: schedule: interval: daily open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 49eba7f..faa8363 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,4 +31,5 @@ issues: exclude-rules: - path: (.+)_test.go linters: + - gochecknoglobals - dupl diff --git a/bench_test.go b/bench_test.go index 06cf3a0..87e5339 100644 --- a/bench_test.go +++ b/bench_test.go @@ -13,7 +13,7 @@ import ( func BenchmarkPacker(b *testing.B) { items := make([]*boxpacker3.Item, 0, 100) - for range 100 { + for range cap(items) { w, _ := rand.Int(rand.Reader, big.NewInt(150)) l, _ := rand.Int(rand.Reader, big.NewInt(150)) h, _ := rand.Int(rand.Reader, big.NewInt(150)) @@ -31,6 +31,7 @@ func BenchmarkPacker(b *testing.B) { boxes := NewDefaultBoxList() packer := boxpacker3.NewPacker() + b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/box.go b/box.go index e308880..ac92b55 100644 --- a/box.go +++ b/box.go @@ -4,34 +4,73 @@ import ( "slices" ) +// Box represents a box that can hold items. +// +// It has fields for the box's dimensions and maximum weight. +// It also has fields for tracking the box's current items and their volume and weight. type Box struct { - id string - width float64 - height float64 - depth float64 + // id is the box's unique identifier. + id string + + // width is the box's width. + width float64 + + // height is the box's height. + height float64 + + // depth is the box's depth. + depth float64 + + // maxWeight is the maximum weight the box can hold. maxWeight float64 - volume float64 - items []*Item - maxLength float64 + // volume is the box's volume (width * height * depth). + volume float64 + + // items is a slice of items currently in the box. + items []*Item + + // maxLength is the length of the box's longest side. + maxLength float64 + + // itemsVolume is the total volume of the items in the box. itemsVolume float64 + + // itemsWeight is the total weight of the items in the box. itemsWeight float64 } +// boxSlice is a slice of boxes. +// +// It implements the sort.Interface by defining Len, Less and Swap methods. type boxSlice []*Box +// Len returns the length of the boxSlice. func (bs boxSlice) Len() int { return len(bs) } +// Less compares two boxes by volume. func (bs boxSlice) Less(i, j int) bool { return bs[i].volume < bs[j].volume } +// Swap swaps two boxes in the boxSlice. func (bs boxSlice) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] } +// NewBox creates a new Box with the given id, dimensions, and maximum weight. +// +// Parameters: +// - id: a unique identifier for the box. +// - w: the width of the box. +// - h: the height of the box. +// - d: the depth of the box. +// - mw: the maximum weight the box can hold. +// +// Returns: +// - A pointer to the newly created Box. func NewBox(id string, w, h, d, mw float64) *Box { //nolint:exhaustruct return &Box{ @@ -46,76 +85,155 @@ func NewBox(id string, w, h, d, mw float64) *Box { } } +// GetID returns the unique identifier of the box. func (b *Box) GetID() string { return b.id } +// GetWidth returns the width of the box. func (b *Box) GetWidth() float64 { return b.width } +// GetHeight returns the height of the box. func (b *Box) GetHeight() float64 { return b.height } +// GetDepth returns the depth of the box. func (b *Box) GetDepth() float64 { return b.depth } +// GetVolume returns the volume of the box. func (b *Box) GetVolume() float64 { return b.volume } +// GetMaxWeight returns the maximum weight the box can hold. func (b *Box) GetMaxWeight() float64 { return b.maxWeight } +// GetItems returns a slice of pointers to the items currently in the box. +// +// The slice is a copy and not a reference to the original slice, so modifying +// the slice returned by this function will not affect the contents of the box. func (b *Box) GetItems() []*Item { - return b.items -} - -// PutItem Attempts to place an element at anchor point p of box b. + return append([]*Item(nil), b.items...) +} + +// PutItem Attempts to place the given item at the specified anchor point within the box. +// +// Attempts to place the given item at the specified anchor point within the box. +// +// It tries to place the item at the given anchor point by iterating through each +// rotation type (Whd, Hwd, Hdw, Dhw, Dwh, Wdh) and checks if the item can be +// placed within the box without intersecting with any of the other items in the box. +// If the item can be placed, it inserts the item into the box and returns true. +// If the item cannot be placed, it returns false. +// +// Parameters: +// - item: The item to be placed in the box. +// - p: The anchor point at which to attempt placing the item within the box. +// +// Returns: +// - bool: True if the item was successfully placed within the box, false otherwise. func (b *Box) PutItem(item *Item, p Pivot) bool { + // Check if the item can fit in the box based on volume and weight quotas. if !b.canQuota(item) { return false } + // Set the item's position to the anchor point. item.position = p + // Iterate through each rotation type to find a suitable placement. for rt := RotationTypeWhd; rt <= RotationTypeWdh; rt++ { + // Set the item's rotation type to the current rotation type. item.rotationType = rt - d := item.GetDimension() - if b.width < p[WidthAxis]+d[WidthAxis] || b.height < p[HeightAxis]+d[HeightAxis] || b.depth < p[DepthAxis]+d[DepthAxis] { + // Get the dimensions of the item in its current rotation type. + itemDimensions := item.GetDimension() + + // Check if the box has enough dimensions to accommodate the item. + if b.width < p[WidthAxis]+itemDimensions[WidthAxis] || + b.height < p[HeightAxis]+itemDimensions[HeightAxis] || + b.depth < p[DepthAxis]+itemDimensions[DepthAxis] { continue } - for _, ib := range b.items { - if ib.Intersect(item) { - return false - } + // Check if the item intersects with any other items in the box. + if b.itemsIntersect(item) { + continue } + // Insert the item into the box and return true. b.insert(item) return true } + // If no suitable placement is found, return false. return false } +// itemsIntersect checks if any of the items in the box intersect with the given item. +// It iterates through each item in the box and calls the Intersect method on the item. +// If an intersection is found, it returns true. +// If no intersection is found, it returns false. +func (b *Box) itemsIntersect(item *Item) bool { + for _, ib := range b.items { + if ib.Intersect(item) { + return true + } + } + + return false +} + +// canQuota checks if the box can accommodate the given item based on both volume and weight quotas. +// +// It calls the canFitVolume and canFitWeight methods to check if the box has enough room for the +// item's volume and weight. If both conditions are true, it returns true. Otherwise, it returns false. func (b *Box) canQuota(item *Item) bool { - return b.itemsVolume+item.volume <= b.volume && b.itemsWeight+item.weight <= b.maxWeight + return b.canFitVolume(item) && b.canFitWeight(item) +} + +// canFitVolume checks if the box can accommodate the given item based on volume. +// +// It compares the sum of the item's volume and the current volume of items in the box +// to the box's total volume. If the sum is less than or equal to the box's total volume, +// it returns true. Otherwise, it returns false. +func (b *Box) canFitVolume(item *Item) bool { + return b.itemsVolume+item.volume <= b.volume +} + +// canFitWeight checks if the box can accommodate the given item based on weight. +// +// It compares the sum of the item's weight and the current weight of items in the box +// to the box's maximum weight. If the sum is less than or equal to the box's maximum weight, +// it returns true. Otherwise, it returns false. +func (b *Box) canFitWeight(item *Item) bool { + return b.itemsWeight+item.weight <= b.maxWeight } +// insert inserts an item into the box and updates the total volume and weight. +// +// It appends the item to the box's items slice and adds the item's volume and weight to the +// box's total volume and weight. func (b *Box) insert(item *Item) { b.items = append(b.items, item) b.itemsVolume += item.volume b.itemsWeight += item.weight } -func (b *Box) purge() { - b.items = make([]*Item, 0, len(b.items)) +// Reset clears the box and resets the volume and weight. +// +// It removes all items from the box by slicing the items slice to an empty slice. +// It sets the total volume and weight of items in the box to 0. +func (b *Box) Reset() { + b.items = b.items[:0] b.itemsVolume = 0 b.itemsWeight = 0 } diff --git a/consts.go b/consts.go index de5fed1..bd17633 100644 --- a/consts.go +++ b/consts.go @@ -1,25 +1,37 @@ package boxpacker3 +// RotationType represents the type of rotation for an item. type RotationType int +// RotationTypeWhd represents the rotation type where the width is the longest dimension. const ( RotationTypeWhd RotationType = iota + // RotationTypeHwd represents the rotation type where the height is the longest dimension. RotationTypeHwd + // RotationTypeHdw represents the rotation type where the depth is the longest dimension. RotationTypeHdw + // RotationTypeDhw represents the rotation type where the depth is the longest dimension. RotationTypeDhw + // RotationTypeDwh represents the rotation type where the width is the longest dimension. RotationTypeDwh + // RotationTypeWdh represents the rotation type where the height is the longest dimension. RotationTypeWdh ) +// Axis represents the axis of a dimension. type Axis int +// WidthAxis represents the width axis. const ( WidthAxis Axis = iota + // HeightAxis represents the height axis. HeightAxis + // DepthAxis represents the depth axis. DepthAxis ) -type ( - Pivot [3]float64 - Dimension [3]float64 -) +// Pivot represents the position of an item within a box. +type Pivot [3]float64 + +// Dimension represents the dimensions of an item or a box. +type Dimension [3]float64 diff --git a/copyptr.go b/copyptr.go index 0b770bc..d4e946b 100644 --- a/copyptr.go +++ b/copyptr.go @@ -1,23 +1,35 @@ package boxpacker3 // CopyPtr creates a copy of a pointer. +// +// It takes a pointer to a generic type T as an argument and returns a new pointer +// to a copy of the original value. If the original pointer is nil, it returns nil. func CopyPtr[T any](original *T) *T { + // If the original pointer is nil, return nil. if original == nil { return nil } + // Create a copy of the value pointed to by the original pointer. copyOfValue := *original + // Return a new pointer to the copied value. return ©OfValue } // CopySlicePtr creates a copy of a slice of pointers. +// +// It takes a slice of pointers as an argument and returns a new slice with the same +// elements, but with each element being a copy of the original. func CopySlicePtr[T any](data []*T) []*T { + // Create a new slice with the same length as the original. result := make([]*T, len(data)) + // Iterate over the original slice and copy each element to the new slice. for i, item := range data { result[i] = CopyPtr(item) } + // Return the new slice. return result } diff --git a/go.mod b/go.mod index 0d8aab2..e0ba463 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.22 require ( github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 - golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) require ( diff --git a/go.sum b/go.sum index 93e963a..5697539 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/item.go b/item.go index c0fcf89..c918211 100644 --- a/item.go +++ b/item.go @@ -1,10 +1,6 @@ package boxpacker3 -import ( - "math" - "slices" -) - +// Item represents an item that can be packed into a box. type Item struct { id string width float64 @@ -18,20 +14,25 @@ type Item struct { position Pivot } +// itemSlice is a slice of items. type itemSlice []*Item +// Len returns the length of the slice. func (it itemSlice) Len() int { return len(it) } +// Less returns true if the volume of the item at index i is less than the volume of the item at index j. func (it itemSlice) Less(i, j int) bool { return it[i].volume < it[j].volume } +// Swap swaps the items at index i and j. func (it itemSlice) Swap(i, j int) { it[i], it[j] = it[j], it[i] } +// NewItem creates a new item with the given parameters. func NewItem(id string, w, h, d, wg float64) *Item { //nolint:exhaustruct return &Item{ @@ -41,76 +42,114 @@ func NewItem(id string, w, h, d, wg float64) *Item { depth: d, weight: wg, volume: w * h * d, - maxLength: slices.Max([]float64{w, h, d}), + maxLength: max(w, h, d), } } +// GetID returns the id of the item. func (i *Item) GetID() string { return i.id } +// GetWidth returns the width of the item. func (i *Item) GetWidth() float64 { return i.width } +// GetHeight returns the height of the item. func (i *Item) GetHeight() float64 { return i.height } +// GetDepth returns the depth of the item. func (i *Item) GetDepth() float64 { return i.depth } +// GetVolume returns the volume of the item. func (i *Item) GetVolume() float64 { return i.volume } +// GetWeight returns the weight of the item. func (i *Item) GetWeight() float64 { return i.weight } +// GetPosition returns the position of the item. func (i *Item) GetPosition() Pivot { return i.position } +// GetDimension returns the dimensions of the item based on its current rotation type. +// +// The dimensions are returned as a Dimension struct. +// +// Returns: +// - Dimension: The dimensions of the item. func (i *Item) GetDimension() Dimension { + // Get the dimensions based on the rotation type of the item. switch i.rotationType { - case RotationTypeWhd: + case RotationTypeWhd: // Width, Height, Depth return Dimension{i.GetWidth(), i.GetHeight(), i.GetDepth()} - case RotationTypeHwd: + case RotationTypeHwd: // Height, Width, Depth return Dimension{i.GetHeight(), i.GetWidth(), i.GetDepth()} - case RotationTypeHdw: + case RotationTypeHdw: // Height, Depth, Width return Dimension{i.GetHeight(), i.GetDepth(), i.GetWidth()} - case RotationTypeDhw: + case RotationTypeDhw: // Depth, Height, Width return Dimension{i.GetDepth(), i.GetHeight(), i.GetWidth()} - case RotationTypeDwh: + case RotationTypeDwh: // Depth, Width, Height return Dimension{i.GetDepth(), i.GetWidth(), i.GetHeight()} - case RotationTypeWdh: + case RotationTypeWdh: // Width, Depth, Height return Dimension{i.GetWidth(), i.GetDepth(), i.GetHeight()} default: // RotationTypeWhd return Dimension{i.GetWidth(), i.GetHeight(), i.GetDepth()} } } -// Intersect Tests for intersections between the i element and the it element. +// Intersect tests for intersections between two items. +// +// It checks for intersections between the current item and the given item. +// It does this by getting the dimensions of the current item and the given item +// and then calling the intersect method with the appropriate parameters. +// +// Parameters: +// - it: The item to check for intersections with. +// +// Returns: +// - bool: True if the items intersect, false otherwise. func (i *Item) Intersect(it *Item) bool { + // Get the dimensions of the current item and the given item. d1 := i.GetDimension() d2 := it.GetDimension() + // Check for intersections in the x and y axes of the two items. return i.intersect(d1, d2, it, WidthAxis, HeightAxis) && i.intersect(d1, d2, it, HeightAxis, DepthAxis) && i.intersect(d1, d2, it, WidthAxis, DepthAxis) } // intersect Checks if two rectangles intersect from the x and y axes of elements i1 and i2. +// +// This function takes the following parameters: +// - d1: The dimensions of the first rectangle. +// - d2: The dimensions of the second rectangle. +// - it: The second rectangle. +// - x: The x axis for the intersection check. +// - y: The y axis for the intersection check. +// +// It returns true if the rectangles intersect, false otherwise. func (i *Item) intersect(d1, d2 Dimension, it *Item, x, y Axis) bool { + // Calculate the center points of the two rectangles. cx1 := i.position[x] + d1[x]/2 //nolint:mnd cy1 := i.position[y] + d1[y]/2 //nolint:mnd cx2 := it.position[x] + d2[x]/2 //nolint:mnd cy2 := it.position[y] + d2[y]/2 //nolint:mnd - ix := math.Max(cx1, cx2) - math.Min(cx1, cx2) - iy := math.Max(cy1, cy2) - math.Min(cy1, cy2) + // Calculate the intersection points on the x and y axes. + ix := max(cx1, cx2) - min(cx1, cx2) + iy := max(cy1, cy2) - min(cy1, cy2) + // Check if the rectangles intersect. return ix < (d1[x]+d2[x])/2 && iy < (d1[y]+d2[y])/2 } diff --git a/packer.go b/packer.go index 549c758..b6d47d9 100644 --- a/packer.go +++ b/packer.go @@ -1,6 +1,7 @@ package boxpacker3 import ( + "context" "slices" "sort" ) @@ -34,6 +35,48 @@ func NewPacker() *Packer { return &Packer{} } +// PackCtx packs items into boxes with a context. +// +// This function sorts input boxes and items by volume and weight. +// It selects the box with the largest volume and weight that +// can accommodate the items. If there are still items left +// after packing the boxes, they will be set as unfit items. +// +// It utilizes a goroutine and a channel to perform the packing process +// asynchronously and to handle the context. +// +// Parameters: +// - ctx: the context.Context to use. +// - inputBoxes: a list of boxes. +// - inputItems: a list of items. +// +// Returns: +// - *Result: the result of packing items into boxes. +// - error: If the context is done before the packing process is complete, +// an error will be returned. nil will be returned otherwise. +func (p *Packer) PackCtx(ctx context.Context, inputBoxes []*Box, inputItems []*Item) (*Result, error) { + // Create a channel to receive the result of the packing process. + result := make(chan *Result) + + // Start a goroutine to perform the packing process. + go func() { + // Perform the packing process. + result <- p.Pack(inputBoxes, inputItems) + // Close the channel to indicate that the packing process is complete. + close(result) + }() + + // Wait for the context to be done or the packing process to complete. + select { + case <-ctx.Done(): + // If the context is done, return nil. + return nil, ctx.Err() + case res := <-result: + // If the packing process is complete, return the result. + return res, nil + } +} + // Pack packs items into boxes. // // This function sorts input boxes and items by volume and weight. @@ -60,7 +103,7 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) *Result { // Create a new Result struct with empty slices. result := &Result{ - UnfitItems: nil, + UnfitItems: make(itemSlice, 0, len(items)), Boxes: p.preferredSort(boxes, items), } @@ -97,26 +140,31 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) *Result { // If there is no box that can accommodate the items, the original // slice of boxes is returned. func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { - volume := 0. - weight := 0. - maxLength := 0. + // Calculate the max volume, weight and maxLength of items. + volume := 0.0 + weight := 0.0 + maxLength := 0.0 for _, item := range items { volume += item.GetVolume() weight += item.GetWeight() - // optimize if maxLength < item.maxLength { maxLength = item.maxLength } } + // Find the preferred box for i, b := range boxes { + // Check if the box can accommodate the items. if b.volume >= volume && b.maxWeight >= weight && b.maxLength >= maxLength { + // If the box can accommodate the items, return the box as the preferred box + // and the remaining boxes sorted after the preferred box. return append(boxSlice{b}, slices.Delete(boxes, i, i+1)...) } } + // If there is no box that can accommodate the items, return the original slice of boxes. return boxes } @@ -155,21 +203,14 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { dimension := b.items[j].GetDimension() // for each axis - for _, pt := range []Axis{WidthAxis, HeightAxis, DepthAxis} { + for _, axis := range []Axis{WidthAxis, HeightAxis, DepthAxis} { // calculate pivot position pv[WidthAxis] = b.items[j].position[WidthAxis] pv[HeightAxis] = b.items[j].position[HeightAxis] pv[DepthAxis] = b.items[j].position[DepthAxis] // add item dimension to pivot position - switch pt { - case WidthAxis: - pv[WidthAxis] += dimension[WidthAxis] - case HeightAxis: - pv[HeightAxis] += dimension[HeightAxis] - case DepthAxis: - pv[DepthAxis] += dimension[DepthAxis] - } + pv[axis] += dimension[axis] // if item can be put in the box if b.PutItem(i, pv) { @@ -187,7 +228,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { copyItems := CopySlicePtr(b.items) // clean box - b.purge() + b.Reset() // try to put item into the box if b.PutItem(i, Pivot{}) { diff --git a/packer_test.go b/packer_test.go index 482ea85..02bfe88 100644 --- a/packer_test.go +++ b/packer_test.go @@ -51,22 +51,32 @@ const ( BoxTypeNotStd6 = "981ffb30-a7b9-4d9e-820e-04de2145763e" ) +var defaultBoxes = []struct { + Type string + W, H, L, MaxWeight float64 +}{ + {BoxTypeF, 220, 185, 50, 20000}, + {BoxTypeE, 165, 215, 100, 20000}, + {BoxTypeG, 265, 165, 190, 20000}, + {BoxTypeC, 425, 165, 190, 20000}, + {BoxTypeB, 425, 265, 190, 20000}, + {BoxTypeA, 425, 265, 380, 20000}, + {BoxTypeStd, 530, 380, 265, 20000}, + {BoxTypeNotStd1, 1000, 500, 500, 20000}, + {BoxTypeNotStd2, 1000, 1000, 1000, 20000}, + {BoxTypeNotStd3, 2000, 500, 500, 20000}, + {BoxTypeNotStd4, 2000, 2000, 2000, 20000}, + {BoxTypeNotStd5, 2500, 2500, 2500, 20000}, + {BoxTypeNotStd6, 3000, 3000, 3000, 20000}, +} + func NewDefaultBoxList() []*boxpacker3.Box { - return []*boxpacker3.Box{ - boxpacker3.NewBox(BoxTypeF, 220, 185, 50, 20000), // 0 - boxpacker3.NewBox(BoxTypeE, 165, 215, 100, 20000), // 1 - boxpacker3.NewBox(BoxTypeG, 265, 165, 190, 20000), // 2 - boxpacker3.NewBox(BoxTypeC, 425, 165, 190, 20000), // 3 - boxpacker3.NewBox(BoxTypeB, 425, 265, 190, 20000), // 4 - boxpacker3.NewBox(BoxTypeA, 425, 265, 380, 20000), // 5 - boxpacker3.NewBox(BoxTypeStd, 530, 380, 265, 20000), // 6 - boxpacker3.NewBox(BoxTypeNotStd1, 1000, 500, 500, 20000), // 7 - boxpacker3.NewBox(BoxTypeNotStd2, 1000, 1000, 1000, 20000), // 8 - boxpacker3.NewBox(BoxTypeNotStd3, 2000, 500, 500, 20000), // 9 - boxpacker3.NewBox(BoxTypeNotStd4, 2000, 2000, 2000, 20000), // 10 - boxpacker3.NewBox(BoxTypeNotStd5, 2500, 2500, 2500, 20000), // 11 - boxpacker3.NewBox(BoxTypeNotStd6, 3000, 3000, 3000, 20000), // 12 + boxes := make([]*boxpacker3.Box, 0, len(defaultBoxes)) + for _, box := range defaultBoxes { + boxes = append(boxes, boxpacker3.NewBox(box.Type, box.W, box.H, box.L, box.MaxWeight)) } + + return boxes } type PackerSuit struct { From 90f990a890557a4aa83ee5bb465e6e2c983f7528 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 21:07:26 +0300 Subject: [PATCH 3/8] memory leak fix --- packer.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packer.go b/packer.go index b6d47d9..36e510e 100644 --- a/packer.go +++ b/packer.go @@ -35,16 +35,13 @@ func NewPacker() *Packer { return &Packer{} } -// PackCtx packs items into boxes with a context. +// PackCtx packs items into boxes asynchronously and handles the context. // // This function sorts input boxes and items by volume and weight. // It selects the box with the largest volume and weight that // can accommodate the items. If there are still items left // after packing the boxes, they will be set as unfit items. // -// It utilizes a goroutine and a channel to perform the packing process -// asynchronously and to handle the context. -// // Parameters: // - ctx: the context.Context to use. // - inputBoxes: a list of boxes. @@ -54,16 +51,18 @@ func NewPacker() *Packer { // - *Result: the result of packing items into boxes. // - error: If the context is done before the packing process is complete, // an error will be returned. nil will be returned otherwise. +// +// This function is useful when you want to pack items into boxes +// asynchronously and handle the context at the same time. func (p *Packer) PackCtx(ctx context.Context, inputBoxes []*Box, inputItems []*Item) (*Result, error) { // Create a channel to receive the result of the packing process. result := make(chan *Result) + defer close(result) // Start a goroutine to perform the packing process. go func() { // Perform the packing process. result <- p.Pack(inputBoxes, inputItems) - // Close the channel to indicate that the packing process is complete. - close(result) }() // Wait for the context to be done or the packing process to complete. From fbc72f7774507d5caedcb440f773a9895cfbce31 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 21:35:55 +0300 Subject: [PATCH 4/8] refactoring & godocs --- bench_test.go | 14 +++ packer.go | 4 +- packer_test.go | 307 +++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 264 insertions(+), 61 deletions(-) diff --git a/bench_test.go b/bench_test.go index 87e5339..656980e 100644 --- a/bench_test.go +++ b/bench_test.go @@ -10,15 +10,25 @@ import ( "github.com/bavix/boxpacker3" ) +// BenchmarkPacker benchmarks the Pack method of the Packer type. +// +// This benchmark uses a list of 100 random items and the default box list. +// The benchmark reports the number of allocations and resets the timer before +// the loop. +// +// The loop iterates over the items and packs them into boxes. func BenchmarkPacker(b *testing.B) { + // Create a slice of 100 random items items := make([]*boxpacker3.Item, 0, 100) for range cap(items) { + // Generate random dimensions for the item w, _ := rand.Int(rand.Reader, big.NewInt(150)) l, _ := rand.Int(rand.Reader, big.NewInt(150)) h, _ := rand.Int(rand.Reader, big.NewInt(150)) w2, _ := rand.Int(rand.Reader, big.NewInt(100)) + // Create a new item with the random dimensions items = append(items, boxpacker3.NewItem( uuid.New().String(), float64(w.Int64()), @@ -28,12 +38,16 @@ func BenchmarkPacker(b *testing.B) { )) } + // Create a new box list with the default boxes boxes := NewDefaultBoxList() + // Create a new packer packer := boxpacker3.NewPacker() + // Report allocations and reset the timer b.ReportAllocs() b.ResetTimer() + // Iterate over the items and pack them into boxes for i := 0; i < b.N; i++ { _ = packer.Pack(boxes, items) } diff --git a/packer.go b/packer.go index 36e510e..e88f9c6 100644 --- a/packer.go +++ b/packer.go @@ -157,9 +157,11 @@ func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { for i, b := range boxes { // Check if the box can accommodate the items. if b.volume >= volume && b.maxWeight >= weight && b.maxLength >= maxLength { + result := make(boxSlice, 0, len(boxes)) + // If the box can accommodate the items, return the box as the preferred box // and the remaining boxes sorted after the preferred box. - return append(boxSlice{b}, slices.Delete(boxes, i, i+1)...) + return append(append(result, b), slices.Delete(boxes, i, i+1)...) } } diff --git a/packer_test.go b/packer_test.go index 02bfe88..e8d202a 100644 --- a/packer_test.go +++ b/packer_test.go @@ -70,8 +70,11 @@ var defaultBoxes = []struct { {BoxTypeNotStd6, 3000, 3000, 3000, 20000}, } +// NewDefaultBoxList creates a list of default boxes based on the predefined box types and dimensions. func NewDefaultBoxList() []*boxpacker3.Box { + // Initialize an empty list of boxes boxes := make([]*boxpacker3.Box, 0, len(defaultBoxes)) + // Iterate over the default box configurations and create a new box for each for _, box := range defaultBoxes { boxes = append(boxes, boxpacker3.NewBox(box.Type, box.W, box.H, box.L, box.MaxWeight)) } @@ -79,48 +82,114 @@ func NewDefaultBoxList() []*boxpacker3.Box { return boxes } +// PackerSuit is a test suite for the boxpacker3 package. +// +// It contains a number of tests for the boxpacker3 package, which can be run +// using the go test command. type PackerSuit struct { suite.Suite } +// TestBoxPackerSuite runs the PackerSuit test suite. +// +// It is a test function that uses the testing package to run the PackerSuit +// test suite. This function is used to test the boxpacker3 package. +// +// The test suite contains a number of tests for the boxpacker3 package, which +// can be run using the go test command. +// +// This function takes a testing.T object as a parameter and calls the Run +// function of the suite.Suite type, passing in the testing.T object and a +// pointer to a new PackerSuit instance. func TestBoxPackerSuite(t *testing.T) { + // Run the test suite in parallel t.Parallel() + // Run the PackerSuit test suite and pass in a new PackerSuit instance suite.Run(t, new(PackerSuit)) } +// TestEmptyBoxes tests the Packer.Pack function with an empty list of boxes and items. +// +// It creates a new Packer instance and calls the Pack function with nil for the +// boxes and items parameters. It then verifies that the Pack function returns a +// non-nil PackResult, that the PackResult.Boxes slice is empty, and that the +// PackResult.UnfitItems slice is also empty. func (s *PackerSuit) TestEmptyBoxes() { + // Get the testing.T instance t := s.T() + + // Run the test in parallel t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + // Call the Pack function with nil for the boxes and items parameters packResult := packer.Pack(nil, nil) - require.NotNil(t, packResult) - require.Empty(t, packResult.Boxes) - require.Empty(t, packResult.UnfitItems) + + // Verify the PackResult + require.NotNil(t, packResult, "Pack function returned nil") + require.Empty(t, packResult.Boxes, "PackResult.Boxes is not empty") + require.Empty(t, packResult.UnfitItems, "PackResult.UnfitItems is not empty") } +// TestEmptyItems tests the Packer.Pack function with an empty list of items. +// +// It creates a new Packer instance and calls the Pack function with a non-nil +// boxes parameter (representing a list of boxes) and a nil items parameter. +// It then verifies that the Pack function returns a non-nil PackResult, that +// the PackResult.Boxes slice has the same length as the boxes parameter, and +// that the PackResult.UnfitItems slice is empty. func (s *PackerSuit) TestEmptyItems() { + // Get the testing.T instance t := s.T() + + // Run the test in parallel t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + // Create a list of default boxes boxes := NewDefaultBoxList() + // Call the Pack function with the boxes and nil items parameters packResult := packer.Pack(boxes, nil) - require.NotNil(t, packResult) - require.Len(t, packResult.Boxes, len(boxes)) - require.Empty(t, packResult.UnfitItems) + + // Verify the PackResult + require.NotNil(t, packResult, "Pack function returned nil") + // Verify that the number of boxes is correct + require.Len(t, packResult.Boxes, len(boxes), "PackResult.Boxes has incorrect length") + // Verify that the UnfitItems slice is empty + require.Empty(t, packResult.UnfitItems, "PackResult.UnfitItems is not empty") } +// TestMinBox tests the Packer.Pack function with a single item that is smaller +// than the smallest box in the list of boxes. +// +// It creates a new Packer instance and calls the Pack function with a non-nil +// boxes parameter (representing a list of boxes) and a single item that is +// smaller than the smallest box in the boxes parameter. It then verifies that +// the Pack function returns a non-nil PackResult, that the PackResult.Boxes +// slice has the same length as the boxes parameter, and that the PackResult. +// UnfitItems slice is empty. It also verifies that the number of items in each +// box in the PackResult.Boxes slice is correct. func (s *PackerSuit) TestMinBox() { + // Get the testing.T instance t := s.T() + + // Run the test in parallel t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + + // Create a list of default boxes boxes := NewDefaultBoxList() + + // Create a single item that is smaller than the smallest box in the boxes + // parameter item := boxpacker3.NewItem( uuid.New().String(), 8, @@ -128,20 +197,34 @@ func (s *PackerSuit) TestMinBox() { 5, 384) + // Call the Pack function with the boxes and a single item packResult := packer.Pack(boxes, []*boxpacker3.Item{item}) - require.NotNil(t, packResult) + // Verify the PackResult + require.NotNil(t, packResult, "Pack function returned nil") + // Verify that the number of boxes is correct + require.Len(t, packResult.Boxes, len(boxes), "PackResult.Boxes has incorrect length") + // Verify that the UnfitItems slice is empty + require.Empty(t, packResult.UnfitItems, "PackResult.UnfitItems is not empty") + + // Verify that the number of items in each box is correct checks := map[string]int{ BoxTypeF: 1, } - - require.Empty(t, packResult.UnfitItems) - - for i := range len(packResult.Boxes) { + for i := range packResult.Boxes { require.Len(t, packResult.Boxes[i].GetItems(), checks[packResult.Boxes[i].GetID()]) } } +// TestRotate tests the Packer.Pack function with a list of items that are +// rotated among the boxes. +// +// It creates a new Packer instance and calls the Pack function with a list of +// boxes and items. It then verifies that the Pack function returns a non-nil +// PackResult, that the PackResult.Boxes slice has the same length as the boxes +// parameter, and that the PackResult. UnfitItems slice is empty. It also +// verifies that the number of items in each box in the PackResult.Boxes slice +// is correct. func (s *PackerSuit) TestRotate() { t := s.T() t.Parallel() @@ -171,13 +254,25 @@ func (s *PackerSuit) TestRotate() { } } +// TestStd tests the Packer.Pack function with a list of standard items. +// +// It creates a new Packer instance and calls the Pack function with a list of +// boxes and standard items. It then verifies that the Pack function returns a +// non-nil PackResult, that the PackResult.Boxes slice has the same length as +// the boxes parameter, and that the PackResult. UnfitItems slice is empty. It +// also verifies that the number of items in each box in the PackResult.Boxes +// slice is correct. func (s *PackerSuit) TestStd() { t := s.T() t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + + // Create a list of default boxes boxes := NewDefaultBoxList() + // Create a list of standard items items := []*boxpacker3.Item{ boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), @@ -186,55 +281,93 @@ func (s *PackerSuit) TestStd() { boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), } + // Call the Pack function with the boxes and standard items packResult := packer.Pack(boxes, items) - require.NotNil(t, packResult) + // Verify the PackResult + require.NotNil(t, packResult, "Pack function returned nil") + + // Verify that the number of boxes is correct + require.Len(t, packResult.Boxes, len(boxes), "PackResult.Boxes has incorrect length") + + // Verify that the UnfitItems slice is empty + require.Empty(t, packResult.UnfitItems, "PackResult.UnfitItems is not empty") + + // Verify that the number of items in each box is correct checks := map[string]int{ BoxTypeStd: 5, } - require.Empty(t, packResult.UnfitItems) - - for i := range len(packResult.Boxes) { + for i := range packResult.Boxes { require.Len(t, packResult.Boxes[i].GetItems(), checks[packResult.Boxes[i].GetID()], packResult.Boxes[i].GetID()) } } +// TestBoxTypeF tests the Packer.Pack function with a list of items that can fit +// into box type F. +// +// It creates a new Packer instance and calls the Pack function with a list of +// boxes and items that can fit into box type F. It then verifies that the Pack +// function returns a non-nil PackResult, that the PackResult.Boxes slice has +// the same length as the boxes parameter, and that the PackResult.UnfitItems +// slice is empty. It also verifies that the number of items in each box in the +// PackResult.Boxes slice is correct. func (s *PackerSuit) TestBoxTypeF() { t := s.T() t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + + // Create a list of default boxes boxes := NewDefaultBoxList() + // Create a list of items that can fit into box type F 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), + boxpacker3.NewItem(uuid.New().String(), 100, 100, 5, 2500), // 5 + boxpacker3.NewItem(uuid.New().String(), 100, 5, 100, 2500), // 6 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), // 7 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), // 8 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), // 9 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2500), // 10 // 35 - boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2500), - boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2500), + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2500), // 11 + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2500), // 12 } + // Call the Pack function with the boxes and items packResult := packer.Pack(boxes, items) - require.NotNil(t, packResult) + // Verify the PackResult + require.NotNil(t, packResult, "Pack function returned nil") + + // Verify that the number of boxes is correct + require.Len(t, packResult.Boxes, len(boxes), "PackResult.Boxes has incorrect length") + + // Verify that the UnfitItems slice is empty + require.Empty(t, packResult.UnfitItems, "PackResult.UnfitItems is not empty") + + // Verify that the number of items in each box is correct checks := map[string]int{ BoxTypeF: 8, } - require.Empty(t, packResult.UnfitItems) - - for i := range len(packResult.Boxes) { + for i := range packResult.Boxes { require.Len(t, packResult.Boxes[i].GetItems(), checks[packResult.Boxes[i].GetID()], packResult.Boxes[i].GetID()) } } +// TestBoxTypeF_Weight tests the Packer.Pack function with a list of items that can fit +// into box type F, but with a weight greater than the weight of box type E. +// +// It creates a new Packer instance and calls the Pack function with a list of boxes +// and items that can fit into box type F, but with a weight greater than the weight +// of box type E. It then verifies that the Pack function returns a non-nil PackResult, +// that the PackResult.Boxes slice has the same length as the boxes parameter, and +// that the PackResult.UnfitItems slice is empty. It also verifies that the number +// of items in each box in the PackResult.Boxes slice is correct. func (s *PackerSuit) TestBoxTypeF_Weight() { t := s.T() t.Parallel() @@ -244,53 +377,70 @@ func (s *PackerSuit) TestBoxTypeF_Weight() { 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), + boxpacker3.NewItem(uuid.New().String(), 100, 100, 5, 2690), // 5 + boxpacker3.NewItem(uuid.New().String(), 100, 5, 100, 2690), // 6 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), // 7 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), // 8 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), // 9 + boxpacker3.NewItem(uuid.New().String(), 5, 100, 100, 2690), // 10 // 35 - boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2690), - boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2690), // maxWeight > 20_000 + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2690), // 11 + boxpacker3.NewItem(uuid.New().String(), 35, 100, 100, 2690), // 12 } packResult := packer.Pack(boxes, items) - require.NotNil(t, packResult) + require.NotNil(t, packResult, "Pack function returned nil") + + // Verify that the number of boxes is correct + require.Len(t, packResult.Boxes, len(boxes), "PackResult.Boxes has incorrect length") + // Verify that the UnfitItems slice is empty + require.Empty(t, packResult.UnfitItems, "PackResult.UnfitItems is not empty") + + // Verify that the number of items in each box is correct checks := map[string]int{ BoxTypeF: 7, BoxTypeE: 1, } - require.Empty(t, packResult.UnfitItems) - - for i := range len(packResult.Boxes) { + for i := range packResult.Boxes { require.Len(t, packResult.Boxes[i].GetItems(), checks[packResult.Boxes[i].GetID()], packResult.Boxes[i].GetID()) } } +// TestPacker_AllBoxes tests the Packer.Pack function with a variety of different +// boxes and items. It verifies that the number of boxes, unfit items, and number +// of items in each box are correct. +// +// It uses a predefined list of boxes and items and checks that the packer correctly +// assigns each item to a box. func (s *PackerSuit) TestPacker_AllBoxes() { t := s.T() t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + // Create a list of boxes boxes := NewDefaultBoxList() - reverse := make([]*boxpacker3.Box, len(boxes)) + // Create a reverse list of boxes (used to iterate over the boxes in reverse order) + reverse := make([]*boxpacker3.Box, len(boxes)) for i := range boxes { reverse[i] = boxes[len(boxes)-1-i] } + // Create a list of items items := []*boxpacker3.Item{ + // Large items boxpacker3.NewItem(uuid.New().String(), 1000, 1000, 1000, 20000), boxpacker3.NewItem(uuid.New().String(), 2000, 500, 500, 20000), boxpacker3.NewItem(uuid.New().String(), 2000, 2000, 2000, 20000), boxpacker3.NewItem(uuid.New().String(), 2500, 2500, 2500, 20000), boxpacker3.NewItem(uuid.New().String(), 3000, 3000, 3000, 20000), + // Small items boxpacker3.NewItem(uuid.New().String(), 220, 185, 50, 20000), boxpacker3.NewItem(uuid.New().String(), 165, 215, 100, 20000), boxpacker3.NewItem(uuid.New().String(), 265, 165, 190, 20000), @@ -301,62 +451,93 @@ func (s *PackerSuit) TestPacker_AllBoxes() { boxpacker3.NewItem(uuid.New().String(), 1000, 500, 500, 20000), } + // Pack the items into the boxes packResult := packer.Pack(boxes, items) - require.NotNil(t, packResult) - require.Empty(t, packResult.UnfitItems) + // Verify that the packing was successful + require.NotNil(t, packResult, "Pack function returned nil") - for i := range len(packResult.Boxes) { - require.Len(t, packResult.Boxes[i].GetItems(), 1) + // Verify that the number of boxes is correct + require.Len(t, packResult.Boxes, len(boxes), "PackResult.Boxes has incorrect length") + + // Verify that there are no unfit items + require.Empty(t, packResult.UnfitItems, "PackResult.UnfitItems is not empty") + + // Verify that the number of items in each box is correct + for i := range packResult.Boxes { + // Each box should contain exactly one item + require.Len(t, packResult.Boxes[i].GetItems(), 1, packResult.Boxes[i].GetID()) } } +// TestPacker_UnfitItems tests the Packer.Pack function with items that don't +// fit into any of the boxes. It verifies that the number of unfit items is +// correct and that no items are packed into any boxes. func (s *PackerSuit) TestPacker_UnfitItems() { t := s.T() t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + + // Create a list of boxes boxes := NewDefaultBoxList() + // Create a list of items that don't fit into any of the boxes 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), + boxpacker3.NewItem(uuid.New().String(), 3001, 3000, 3000, 20000), // Too large in all dimensions + boxpacker3.NewItem(uuid.New().String(), 3000, 3001, 3000, 20000), // Too large in one dimension + boxpacker3.NewItem(uuid.New().String(), 3000, 3000, 3001, 20000), // Too large in another dimension + boxpacker3.NewItem(uuid.New().String(), 3000, 3000, 3000, 20001), // Too heavy } + // Pack the items into the boxes packResult := packer.Pack(boxes, items) - require.NotNil(t, packResult) - require.Len(t, packResult.UnfitItems, 4) + // Verify that the packing was successful + require.NotNil(t, packResult, "Pack function returned nil") - for i := range len(packResult.Boxes) { - require.Empty(t, packResult.Boxes[i].GetItems(), packResult.Boxes[i].GetID()) + // Verify that the number of unfit items is correct + require.Len(t, packResult.UnfitItems, 4, "PackResult.UnfitItems has incorrect length") + + // Verify that no items are packed into any boxes + for i := range packResult.Boxes { + require.Empty(t, packResult.Boxes[i].GetItems(), packResult.Boxes[i].GetID(), "Box "+packResult.Boxes[i].GetID()+" contains items") } } +// TestPacker_MinAndStd tests the Packer.Pack function with a combination of +// standard and minimum sized items. It verifies that the packing is correct +// and that the number of items in each box is correct. +// +//nolint:funlen func (s *PackerSuit) TestPacker_MinAndStd() { t := s.T() t.Parallel() + // Create a new Packer instance packer := boxpacker3.NewPacker() + // Create a list of boxes boxes := NewDefaultBoxList() reverse := make([]*boxpacker3.Box, len(boxes)) + // Reverse the list of boxes to guarantee that the packer will + // pack items into boxes based on their volume, not their dimensions for i := range boxes { reverse[i] = boxes[len(boxes)-1-i] } + // Create a list of items items := []*boxpacker3.Item{ - // std + // Standard size items boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), // 1 boxpacker3.NewItem(uuid.New().String(), 380, 100, 250, 2690), // 2 boxpacker3.NewItem(uuid.New().String(), 250, 380, 100, 2690), // 3 boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), // 4 boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), // 5 - // min + // Minimum size items boxpacker3.NewItem(uuid.New().String(), 220, 185, 50, 20000), // 6. F boxpacker3.NewItem(uuid.New().String(), 165, 215, 100, 20000), // 7. E boxpacker3.NewItem(uuid.New().String(), 265, 165, 190, 20000), // 8. G @@ -366,13 +547,20 @@ func (s *PackerSuit) TestPacker_MinAndStd() { boxpacker3.NewItem(uuid.New().String(), 530, 380, 265, 20000), // 12. Std boxpacker3.NewItem(uuid.New().String(), 1000, 500, 500, 20000), // 13. NotStd1 - // max + // Maximum size items boxpacker3.NewItem(uuid.New().String(), 3000, 3000, 3000, 20000), // 14. NotStd6 } - packResult := packer.Pack(boxes, items) - require.NotNil(t, packResult) + // Pack the items into the boxes + packResult := packer.Pack(reverse, items) + + // Verify that the packing was successful + require.NotNil(t, packResult, "Pack function returned nil") + + // Verify that there are no unfit items + require.Empty(t, packResult.UnfitItems) + // Define the expected number of items in each box checks := map[string]int{ BoxTypeF: 1, // 1 BoxTypeE: 1, // 2 @@ -388,9 +576,8 @@ func (s *PackerSuit) TestPacker_MinAndStd() { BoxTypeNotStd6: 1, // 14 } - require.Empty(t, packResult.UnfitItems) - - for i := range len(packResult.Boxes) { + // Verify that the number of items in each box is correct + for i := range packResult.Boxes { require.Len(t, packResult.Boxes[i].GetItems(), checks[packResult.Boxes[i].GetID()], packResult.Boxes[i].GetID()) } } From 002bafd32b8255035d26a9f49ef3c12edba0215a Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 22:22:33 +0300 Subject: [PATCH 5/8] update --- box.go | 8 ++--- packer.go | 102 +++++++++++++++++++++++++++--------------------------- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/box.go b/box.go index ac92b55..63056cd 100644 --- a/box.go +++ b/box.go @@ -1,9 +1,5 @@ package boxpacker3 -import ( - "slices" -) - // Box represents a box that can hold items. // // It has fields for the box's dimensions and maximum weight. @@ -79,9 +75,9 @@ func NewBox(id string, w, h, d, mw float64) *Box { height: h, depth: d, maxWeight: mw, - maxLength: slices.Max([]float64{w, h, d}), + maxLength: max(w, h, d), volume: w * h * d, - items: nil, + items: make([]*Item, 0, 1), } } diff --git a/packer.go b/packer.go index e88f9c6..c5140ee 100644 --- a/packer.go +++ b/packer.go @@ -56,14 +56,10 @@ func NewPacker() *Packer { // asynchronously and handle the context at the same time. func (p *Packer) PackCtx(ctx context.Context, inputBoxes []*Box, inputItems []*Item) (*Result, error) { // Create a channel to receive the result of the packing process. - result := make(chan *Result) - defer close(result) + result := make(chan *Result, 1) // Start a goroutine to perform the packing process. - go func() { - // Perform the packing process. - result <- p.Pack(inputBoxes, inputItems) - }() + go func() { result <- p.Pack(inputBoxes, inputItems) }() // Wait for the context to be done or the packing process to complete. select { @@ -118,9 +114,7 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) *Result { } // If there are still items left, set them as unfit items. - if len(items) > 0 { - result.UnfitItems = append(result.UnfitItems, items...) - } + result.UnfitItems = append(result.UnfitItems, items...) return result } @@ -128,34 +122,39 @@ func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) *Result { // preferredSort selects the box with the largest volume and weight // that can accommodate the items. // +// This function calculates the maximum volume, weight, and maximum length of the items. +// It then iterates through the boxes and checks if a box can accommodate all the items. +// If a box is found that can accommodate the items, it is returned as the preferred box. +// The remaining boxes are sorted after the preferred box. +// If no box can accommodate the items, the original slice of boxes is returned. +// // Parameters: // - boxes: a slice of boxes. // - items: a slice of items. // // Returns: -// - a slice of boxes sorted by volume, weight and maxLength. +// - a slice of boxes sorted by volume, weight, and maximum length. // The first box in the slice is the preferred box. // The remaining boxes are sorted after the preferred box. // If there is no box that can accommodate the items, the original // slice of boxes is returned. func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { - // Calculate the max volume, weight and maxLength of items. - volume := 0.0 - weight := 0.0 - maxLength := 0.0 + // Calculate the max volume, weight, and maximum length of the items. + var volume, weight, maxLength float64 + // Iterate through the items and calculate the total volume, weight, and maximum length. for _, item := range items { volume += item.GetVolume() weight += item.GetWeight() - - if maxLength < item.maxLength { - maxLength = item.maxLength - } + maxLength = max(maxLength, item.maxLength) } // Find the preferred box for i, b := range boxes { // Check if the box can accommodate the items. + // A box can accommodate the items if its volume is greater than or equal to the total volume of the items, + // its maximum weight is greater than or equal to the total weight of the items, + // and its maximum length is greater than or equal to the maximum length of the items. if b.volume >= volume && b.maxWeight >= weight && b.maxLength >= maxLength { result := make(boxSlice, 0, len(boxes)) @@ -171,10 +170,10 @@ func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { // packToBox Packs goods in a box b. Returns unpackaged goods. // -// It attempts to pack items into the box. If the box is not big enough -// to accommodate all the items, it tries to pack the items again -// into the box by rotating the box and items. -// If the box is still not big enough, it adds the items to the unpacked slice. +// PackToBox attempts to pack items into the box. If the box is not big enough +// to accommodate all the items, it tries to pack the items again into the box +// by rotating the box and items. If the box is still not big enough, it adds +// the items to the unpacked slice. // // Parameters: // - b: the box to pack items into. @@ -189,32 +188,33 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { unpacked := make([]*Item, 0, len(items)) pv := Pivot{} + index := 0 - // if box is empty, first item is put in box - if b.items == nil && len(items) > 0 && b.PutItem(items[0], pv) { - items = items[1:] + // If box is empty, first item is put in box + if b.items == nil && len(items) > 0 && b.PutItem(items[index], pv) { + index++ } - // try to pack all items into the box - for _, i := range items { + // Try to pack all items into the box + for i := index; i < len(items); i++ { fitted = false - // for each item already in the box + // Try to put item into the box for j := range b.items { dimension := b.items[j].GetDimension() - // for each axis + // Try to put item in each axis for _, axis := range []Axis{WidthAxis, HeightAxis, DepthAxis} { - // calculate pivot position + // Calculate pivot position pv[WidthAxis] = b.items[j].position[WidthAxis] pv[HeightAxis] = b.items[j].position[HeightAxis] pv[DepthAxis] = b.items[j].position[DepthAxis] - // add item dimension to pivot position + // Add item dimension to pivot position pv[axis] += dimension[axis] - // if item can be put in the box - if b.PutItem(i, pv) { + // If item can be put in the box + if b.PutItem(items[i], pv) { fitted = true break @@ -222,34 +222,34 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { } } - // if item can not be put in the box + // If item cannot be put in the box if !fitted { - // make a backup of box + // Make a backup of box backup := CopyPtr(b) copyItems := CopySlicePtr(b.items) - // clean box + // Clean box b.Reset() - // try to put item into the box - if b.PutItem(i, Pivot{}) { - // count of items fit in the box + // Try to put item into the box + if b.PutItem(items[i], Pivot{}) { + // Count of items fit in the box itemsFit := 0 - // for each item that was in the box + // Try to put each item in the box for k := 0; k < len(copyItems) && itemsFit < len(copyItems); k++ { - // try to put item in the box + // Try to put item in the box for j := len(b.items) - 1; j >= 0; j-- { dimension := b.items[j].GetDimension() - // for each axis + // Try to put item in each axis for _, pt := range []Axis{WidthAxis, HeightAxis, DepthAxis} { - // calculate pivot position + // Calculate pivot position pv[WidthAxis] = b.items[j].position[WidthAxis] pv[HeightAxis] = b.items[j].position[HeightAxis] pv[DepthAxis] = b.items[j].position[DepthAxis] - // add item dimension to pivot position + // Add item dimension to pivot position switch pt { case WidthAxis: pv[WidthAxis] += dimension[WidthAxis] @@ -259,7 +259,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { pv[DepthAxis] += dimension[DepthAxis] } - // if item can be put in the box + // If item can be put in the box if b.PutItem(copyItems[k], pv) { itemsFit++ @@ -269,21 +269,21 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { } } - // if all items that were in the box now fit in the box + // If all items that were in the box now fit in the box fitted = itemsFit == len(copyItems) } - // if not all items that were in the box now fit in the box + // If not all items that were in the box now fit in the box if !fitted { - // restore box from backup + // Restore box from backup *b = *backup } } - // if item can not be put in the box + // If item cannot be put in the box if !fitted { - // add item to unpacked slice - unpacked = append(unpacked, i) + // Add item to unpacked slice + unpacked = append(unpacked, items[i]) } } From cf4a56cc10180d8259693edf7c4924a71751872a Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 22:32:10 +0300 Subject: [PATCH 6/8] reverse logic --- packer.go | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/packer.go b/packer.go index c5140ee..e69d6c1 100644 --- a/packer.go +++ b/packer.go @@ -228,26 +228,25 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { backup := CopyPtr(b) copyItems := CopySlicePtr(b.items) - // Clean box - b.Reset() + backup.Reset() - // Try to put item into the box - if b.PutItem(items[i], Pivot{}) { + // Try to put item into the backup box + if backup.PutItem(items[i], Pivot{}) { // Count of items fit in the box itemsFit := 0 // Try to put each item in the box for k := 0; k < len(copyItems) && itemsFit < len(copyItems); k++ { // Try to put item in the box - for j := len(b.items) - 1; j >= 0; j-- { - dimension := b.items[j].GetDimension() + for j := len(backup.items) - 1; j >= 0; j-- { + dimension := backup.items[j].GetDimension() // Try to put item in each axis for _, pt := range []Axis{WidthAxis, HeightAxis, DepthAxis} { // Calculate pivot position - pv[WidthAxis] = b.items[j].position[WidthAxis] - pv[HeightAxis] = b.items[j].position[HeightAxis] - pv[DepthAxis] = b.items[j].position[DepthAxis] + pv[WidthAxis] = backup.items[j].position[WidthAxis] + pv[HeightAxis] = backup.items[j].position[HeightAxis] + pv[DepthAxis] = backup.items[j].position[DepthAxis] // Add item dimension to pivot position switch pt { @@ -260,7 +259,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { } // If item can be put in the box - if b.PutItem(copyItems[k], pv) { + if backup.PutItem(copyItems[k], pv) { itemsFit++ break @@ -271,12 +270,11 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // If all items that were in the box now fit in the box fitted = itemsFit == len(copyItems) - } - // If not all items that were in the box now fit in the box - if !fitted { - // Restore box from backup - *b = *backup + // If successfully filled, restore backup in b + if fitted { + *b = *backup + } } } @@ -289,4 +287,3 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // return unpacked slice return unpacked -} From 9046fa04b0f96e9d33dd2b4654ed9ec32dc34411 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 22:35:07 +0300 Subject: [PATCH 7/8] add pos zero --- packer.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packer.go b/packer.go index e69d6c1..cdbff07 100644 --- a/packer.go +++ b/packer.go @@ -237,10 +237,15 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // Try to put each item in the box for k := 0; k < len(copyItems) && itemsFit < len(copyItems); k++ { - // Try to put item in the box for j := len(backup.items) - 1; j >= 0; j-- { dimension := backup.items[j].GetDimension() + // Check if item can be put in the box + if backup.PutItem(copyItems[k], backup.items[j].position) { + itemsFit++ + break + } + // Try to put item in each axis for _, pt := range []Axis{WidthAxis, HeightAxis, DepthAxis} { // Calculate pivot position @@ -261,7 +266,6 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // If item can be put in the box if backup.PutItem(copyItems[k], pv) { itemsFit++ - break } } @@ -287,3 +291,4 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // return unpacked slice return unpacked +} From b87ffd6097bb5e4e6eda5dc65d19b691e6f6ffca Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Fri, 12 Jul 2024 22:39:22 +0300 Subject: [PATCH 8/8] lint --- packer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packer.go b/packer.go index cdbff07..7402260 100644 --- a/packer.go +++ b/packer.go @@ -182,7 +182,7 @@ func (p *Packer) preferredSort(boxes boxSlice, items itemSlice) boxSlice { // Returns: // - a slice of items that did not fit into the box. // -//nolint:funlen,gocognit,cyclop +//nolint:funlen,gocognit,cyclop,nestif func (p *Packer) packToBox(b *Box, items []*Item) []*Item { var fitted bool @@ -243,6 +243,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // Check if item can be put in the box if backup.PutItem(copyItems[k], backup.items[j].position) { itemsFit++ + break } @@ -266,6 +267,7 @@ func (p *Packer) packToBox(b *Box, items []*Item) []*Item { // If item can be put in the box if backup.PutItem(copyItems[k], pv) { itemsFit++ + break } }