box_packer 0.0.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|