box_packer 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -13
- data/.gitignore +0 -16
- data/.rspec +2 -0
- data/README.md +70 -23
- data/box_packer.gemspec +14 -11
- data/lib/box_packer.rb +3 -196
- data/lib/box_packer/box.rb +39 -0
- data/lib/box_packer/builder.rb +22 -0
- data/lib/box_packer/container.rb +90 -0
- data/lib/box_packer/dimensions.rb +37 -0
- data/lib/box_packer/item.rb +33 -0
- data/lib/box_packer/packer.rb +58 -0
- data/lib/box_packer/packing.rb +40 -0
- data/lib/box_packer/position.rb +11 -0
- data/lib/box_packer/version.rb +1 -1
- data/spec/lib/box_spec.rb +53 -0
- data/spec/lib/container_spec.rb +88 -0
- data/spec/lib/dimensions_spec.rb +52 -0
- data/spec/lib/item_spec.rb +30 -0
- data/spec/lib/packer_spec.rb +81 -0
- data/spec/lib/packing_spec.rb +73 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/matchers/box_packer.rb +40 -0
- metadata +61 -9
- data/lib/tc_box_packer.rb +0 -171
@@ -0,0 +1,90 @@
|
|
1
|
+
require_relative 'box'
|
2
|
+
require_relative 'packing'
|
3
|
+
require_relative 'dimensions'
|
4
|
+
require_relative 'item'
|
5
|
+
require_relative 'packer'
|
6
|
+
|
7
|
+
module BoxPacker
|
8
|
+
|
9
|
+
def self.container(*args, &b)
|
10
|
+
Container.new(*args, &b)
|
11
|
+
end
|
12
|
+
|
13
|
+
class Container < Box
|
14
|
+
attr_accessor :label, :weight_limit, :packings_limit
|
15
|
+
attr_reader :items, :packing, :packings, :packed_successfully
|
16
|
+
|
17
|
+
def initialize(dimensions, opts={}, &b)
|
18
|
+
super(Dimensions[*dimensions])
|
19
|
+
@label = opts[:label]
|
20
|
+
@weight_limit = opts[:weight_limit]
|
21
|
+
@packings_limit = opts[:packings_limit]
|
22
|
+
@items = opts[:items] || []
|
23
|
+
orient!
|
24
|
+
self.instance_exec(&b) if block_given?
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_item(*args)
|
28
|
+
items << Item.new(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def <<(item)
|
32
|
+
items << item.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def items=(new_items)
|
36
|
+
@items = new_items.map(&:dup)
|
37
|
+
end
|
38
|
+
|
39
|
+
def pack!
|
40
|
+
prepare_to_pack!
|
41
|
+
return unless packable?
|
42
|
+
@packed_successfully = Packer.pack(self)
|
43
|
+
packings.count
|
44
|
+
end
|
45
|
+
|
46
|
+
def new_packing!
|
47
|
+
@packing = Packing.new(volume, weight_limit)
|
48
|
+
@packings << @packing
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
s = "\n|Container|"
|
53
|
+
s << " #{label}" if label
|
54
|
+
s << " #{dimensions}"
|
55
|
+
s << " Weight Limit:#{weight_limit}" if weight_limit
|
56
|
+
s << " Packings Limit:#{packings_limit}" if packings_limit
|
57
|
+
s << "\n"
|
58
|
+
s << (@packings ? @packings : items).map(&:to_s).join
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def packable?
|
64
|
+
return false if items.empty?
|
65
|
+
total_weight = 0
|
66
|
+
|
67
|
+
items.each do |item|
|
68
|
+
if weight_limit && item.weight
|
69
|
+
return false if item.weight > weight_limit
|
70
|
+
total_weight += item.weight
|
71
|
+
end
|
72
|
+
|
73
|
+
return false unless self >= item
|
74
|
+
end
|
75
|
+
|
76
|
+
if weight_limit && packings_limit
|
77
|
+
return total_weight <= weight_limit * packings_limit
|
78
|
+
end
|
79
|
+
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
def prepare_to_pack!
|
84
|
+
items.each(&:orient!)
|
85
|
+
@packings = []
|
86
|
+
@packed_successfully = false
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
module BoxPacker
|
4
|
+
class Dimensions < Vector
|
5
|
+
|
6
|
+
def width
|
7
|
+
Dimensions[self[0], 0, 0]
|
8
|
+
end
|
9
|
+
|
10
|
+
def height
|
11
|
+
Dimensions[0, self[1], 0]
|
12
|
+
end
|
13
|
+
|
14
|
+
def depth
|
15
|
+
Dimensions[0, 0, self[2]]
|
16
|
+
end
|
17
|
+
|
18
|
+
def volume
|
19
|
+
@volume ||= self[0] * self[1] * self[2]
|
20
|
+
end
|
21
|
+
|
22
|
+
def >=(other_dimensions)
|
23
|
+
map2(other_dimensions){ |v1, v2| v1 >= v2 }.reduce(&:&)
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_rotation
|
27
|
+
to_a.permutation.each do |perm|
|
28
|
+
yield Dimensions[*perm]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
"#{self[0]}x#{self[1]}x#{self[2]}"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'box'
|
2
|
+
require_relative 'dimensions'
|
3
|
+
|
4
|
+
module BoxPacker
|
5
|
+
class Item < Box
|
6
|
+
attr_accessor :label, :weight
|
7
|
+
|
8
|
+
def initialize(dimensions, opts={})
|
9
|
+
super(Dimensions[*dimensions])
|
10
|
+
@label = opts[:label]
|
11
|
+
@weight = opts[:weight]
|
12
|
+
end
|
13
|
+
|
14
|
+
def fit_into?(box)
|
15
|
+
each_rotation do |rotation|
|
16
|
+
if box.dimensions >= rotation
|
17
|
+
@dimensions = rotation
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
s = '| Item|'
|
26
|
+
s << " #{label}" if label
|
27
|
+
s << " #{dimensions} #{position} Volume:#{volume}"
|
28
|
+
s << " Weight:#{weight}" if weight
|
29
|
+
s << "\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'attr_extras'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module BoxPacker
|
5
|
+
class Packer
|
6
|
+
extend Forwardable
|
7
|
+
method_object :pack, :container
|
8
|
+
def_delegators :container, :new_packing!, :packings_limit, :packings, :packing
|
9
|
+
|
10
|
+
def pack
|
11
|
+
@items = container.items.sort_by!(&:volume).reverse!
|
12
|
+
|
13
|
+
until too_many_packings? do
|
14
|
+
new_packing!
|
15
|
+
pack_box(@items, container)
|
16
|
+
return true if @items.empty?
|
17
|
+
end
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def too_many_packings?
|
24
|
+
packings.count >= packings_limit - 1 if packings_limit
|
25
|
+
end
|
26
|
+
|
27
|
+
def pack_box(possible_items, box)
|
28
|
+
possible_items = possible_items.dup
|
29
|
+
until possible_items.empty?
|
30
|
+
item = possible_items.shift
|
31
|
+
|
32
|
+
if item.fit_into?(box)
|
33
|
+
pack_item!(item, possible_items, box)
|
34
|
+
break if possible_items.empty?
|
35
|
+
|
36
|
+
box.sub_boxes(item).each do |sub_box|
|
37
|
+
purge!(possible_items)
|
38
|
+
pack_box(possible_items, sub_box)
|
39
|
+
end
|
40
|
+
break
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def pack_item!(item, possible_items, box)
|
46
|
+
item.position = box.position
|
47
|
+
possible_items.delete(item)
|
48
|
+
packing << @items.delete(item)
|
49
|
+
end
|
50
|
+
|
51
|
+
def purge!(possible_items)
|
52
|
+
possible_items.keep_if do |item|
|
53
|
+
packing.fit?(item)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module BoxPacker
|
4
|
+
class Packing < SimpleDelegator
|
5
|
+
attr_reader :remaining_weight, :remaining_volume
|
6
|
+
|
7
|
+
def initialize(total_volume, total_weight)
|
8
|
+
super([])
|
9
|
+
@remaining_volume = total_volume
|
10
|
+
@remaining_weight = total_weight
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(item)
|
14
|
+
@remaining_volume -= item.volume
|
15
|
+
@remaining_weight -= item.weight if weight?(item)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def fit?(item)
|
20
|
+
return false if include?(item)
|
21
|
+
return false if remaining_volume < item.volume
|
22
|
+
return false if weight?(item) && remaining_weight < item.weight
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
s = "| Packing| Remaining Volume:#{remaining_volume}"
|
28
|
+
s << " Remaining Weight:#{remaining_weight}" if remaining_weight
|
29
|
+
s << "\n"
|
30
|
+
s << map(&:to_s).join
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def weight?(item)
|
36
|
+
remaining_weight && item.weight
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/box_packer/version.rb
CHANGED
@@ -0,0 +1,53 @@
|
|
1
|
+
module BoxPacker
|
2
|
+
describe Box do
|
3
|
+
subject(:box) { Box.new(dimensions, position: position) }
|
4
|
+
let(:dimensions) { Dimensions[25, 30, 10] }
|
5
|
+
let(:position) { Position[10, 25, 5] }
|
6
|
+
let(:item) { Item.new(Dimensions[5, 2, 1]) }
|
7
|
+
|
8
|
+
context 'if no position is given' do
|
9
|
+
let(:position) { nil }
|
10
|
+
|
11
|
+
it "defaults to origin position" do
|
12
|
+
expect(box.position).to eql(Position[0, 0, 0])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#orient!' do
|
17
|
+
before { box.orient! }
|
18
|
+
it { expect(box.dimensions).to eql(Dimensions[30, 25, 10]) }
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#sub_boxes_args' do
|
22
|
+
let(:expected_args) {[
|
23
|
+
[ Dimensions[20, 30, 10], position: Position[15, 25, 5] ],
|
24
|
+
[ Dimensions[5, 28, 10], position: Position[10, 27, 5] ],
|
25
|
+
[ Dimensions[5, 2, 9], position: Position[10, 25, 6] ]
|
26
|
+
]}
|
27
|
+
|
28
|
+
it 'calculates the correct dimensions and positions' do
|
29
|
+
expect(box.send(:sub_boxes_args, item)).to eql(expected_args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#sub_boxes' do
|
34
|
+
|
35
|
+
it 'orders sub-boxes by volumes' do
|
36
|
+
sub_boxes = box.sub_boxes(item)
|
37
|
+
expect(sub_boxes[0].volume).to be >=(sub_boxes[1].volume)
|
38
|
+
expect(sub_boxes[1].volume).to be >=(sub_boxes[2].volume)
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with an item that reaches a side' do
|
42
|
+
let(:item) { Box.new(Dimensions[15, 2, 10]) }
|
43
|
+
|
44
|
+
it 'only returns 2 sub-boxes' do
|
45
|
+
sub_boxes = box.sub_boxes(item)
|
46
|
+
expect(sub_boxes.length).to eql(2)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module BoxPacker
|
2
|
+
describe Container do
|
3
|
+
subject(:container) { Container.new([25, 30, 15]) }
|
4
|
+
|
5
|
+
it { expect(container.packings).to eql(nil) }
|
6
|
+
|
7
|
+
context 'with items' do
|
8
|
+
let(:items) { [
|
9
|
+
Item.new([15, 24, 8], weight: 25),
|
10
|
+
Item.new([ 2, 1, 2], weight: 6),
|
11
|
+
Item.new([ 9, 9, 10], weight: 30)
|
12
|
+
] }
|
13
|
+
before { container.items = items }
|
14
|
+
|
15
|
+
context 'with container prepared' do
|
16
|
+
before do
|
17
|
+
container.send(:prepare_to_pack!)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#prepare_to_pack!' do
|
21
|
+
let(:expected_dimensions){[
|
22
|
+
Dimensions[24, 15, 8],
|
23
|
+
Dimensions[2, 2, 1],
|
24
|
+
Dimensions[10, 9, 9]
|
25
|
+
]}
|
26
|
+
|
27
|
+
it { expect(items.map(&:dimensions)).to eql(expected_dimensions) }
|
28
|
+
it { expect(container.packings).to eql([]) }
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with some items packed' do
|
32
|
+
before do
|
33
|
+
container.new_packing!
|
34
|
+
container.packing << items[1]
|
35
|
+
container.packing << items[2]
|
36
|
+
end
|
37
|
+
|
38
|
+
it { expect(container.packings.length).to eql(1) }
|
39
|
+
it { expect(container.packing).to match_array([items[1], items[2]]) }
|
40
|
+
|
41
|
+
describe '#new_packing!!' do
|
42
|
+
before { container.new_packing! }
|
43
|
+
it { expect(container.packings.length).to eql(2) }
|
44
|
+
it { expect(container.packing).to eql([]) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#packable?' do
|
50
|
+
|
51
|
+
context 'with no items' do
|
52
|
+
before { container.items = [] }
|
53
|
+
it { expect(container.send(:packable?)).to be(false) }
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'with items that fit' do
|
57
|
+
it { expect(container.send(:packable?)).to be(true) }
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with an item to large' do
|
61
|
+
before { items[0].dimensions = Dimensions[26, 34, 8] }
|
62
|
+
it { expect(container.send(:packable?)).to be(false) }
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'with a weight limit thats lighter than one of the items' do
|
66
|
+
before { container.weight_limit = 24 }
|
67
|
+
it { expect(container.send(:packable?)).to be(false) }
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'with a packings limit of one packing' do
|
71
|
+
before { container.packings_limit = 1 }
|
72
|
+
|
73
|
+
context 'with a weight limit thats lighter than items' do
|
74
|
+
before { container.weight_limit = 50 }
|
75
|
+
it { expect(container.send(:packable?)).to be(false) }
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'with a weight limit thats heavier than items' do
|
79
|
+
before { container.weight_limit = 70 }
|
80
|
+
it { expect(container.send(:packable?)).to be(true) }
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module BoxPacker
|
2
|
+
describe Dimensions do
|
3
|
+
subject(:dimensions){ Dimensions[2, 10, 3] }
|
4
|
+
|
5
|
+
describe '#volume' do
|
6
|
+
it { expect(dimensions.volume).to eql(60) }
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#>=' do
|
10
|
+
|
11
|
+
context 'when compared with Dimensions that are all bigger' do
|
12
|
+
let(:other_dimensions) { Dimensions[1, 5, 2] }
|
13
|
+
it { expect(dimensions >= other_dimensions).to be(true) }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when compared with Dimensions where some are bigger and some are equal' do
|
17
|
+
let(:other_dimensions) { Dimensions[2, 5, 3] }
|
18
|
+
it { expect(dimensions >= other_dimensions).to be(true) }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when compared with Dimensions that are all equal' do
|
22
|
+
let(:other_dimensions) { Dimensions[2, 10, 3] }
|
23
|
+
it { expect(dimensions >= other_dimensions).to be(true) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when compared with Dimensions where some are bigger and some are smaller' do
|
27
|
+
let(:other_dimensions) { Dimensions[5, 5, 1] }
|
28
|
+
it { expect(dimensions >= other_dimensions).to be(false) }
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when compared with Dimensions where none are bigger' do
|
32
|
+
let(:other_dimensions) { Dimensions[5, 15, 11] }
|
33
|
+
it { expect(dimensions >= other_dimensions).to be(false) }
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#each_rotation' do
|
39
|
+
let(:rotations){[
|
40
|
+
Dimensions[2, 10, 3],
|
41
|
+
Dimensions[2, 3, 10],
|
42
|
+
Dimensions[10, 2, 3],
|
43
|
+
Dimensions[10, 3, 2],
|
44
|
+
Dimensions[3, 10, 2],
|
45
|
+
Dimensions[3, 2, 10]
|
46
|
+
]}
|
47
|
+
|
48
|
+
it { expect{|b| dimensions.each_rotation(&b)}.to yield_each_once(rotations) }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|