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.
@@ -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
@@ -0,0 +1,11 @@
1
+ require 'matrix'
2
+
3
+ module BoxPacker
4
+ class Position < Vector
5
+
6
+ def to_s
7
+ "(#{self[0]},#{self[1]},#{self[2]})"
8
+ end
9
+
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module BoxPacker
2
- VERSION = "0.0.2"
2
+ VERSION = '1.0.0'
3
3
  end
@@ -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