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.
@@ -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