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,30 @@
1
+ module BoxPacker
2
+ describe Item do
3
+ subject(:item){ Item.new(dimensions) }
4
+ let(:dimensions){ [12, 5, 7] }
5
+
6
+ describe '#fit_into' do
7
+
8
+ context 'when box is larger than box' do
9
+ let(:box) { Box.new(Dimensions[6, 15, 9]) }
10
+ let(:rotated_dimensions) { Dimensions[5, 12, 7] }
11
+
12
+ it 'fits and orientation is rotated to fit' do
13
+ expect(item.fit_into?(box)).to be(true)
14
+ expect(item.dimensions).to eql(rotated_dimensions)
15
+ end
16
+ end
17
+
18
+ context 'when other box has a smaller side than box' do
19
+ let(:box) { Box.new(Dimensions[11, 6, 7]) }
20
+
21
+ it 'does not fit and orientation is unchanged' do
22
+ expect(item.fit_into?(box)).to be(false)
23
+ expect(item.dimensions).to eql(Dimensions[*dimensions])
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,81 @@
1
+ module BoxPacker
2
+ describe Packer do
3
+ subject(:container) { Container.new(dimensions) }
4
+ let(:dimensions) { [10, 15, 5] }
5
+ let(:opts) { nil }
6
+ let(:items){[
7
+ Item.new([1,1,1], weight: 1),
8
+ Item.new([3,1,1], weight: 6),
9
+ Item.new([5,5,1], weight: 15),
10
+ Item.new([5,5,5], weight: 20)
11
+ ]}
12
+
13
+ describe '#pack' do
14
+
15
+ context 'with items that fit exactly in one packing' do
16
+ before do
17
+ 20.times { container.items << items[0].dup }
18
+ 10.times { container.items << items[1].dup }
19
+ 8.times { container.items << items[2].dup }
20
+ 4.times { container.items << items[3].dup }
21
+ end
22
+
23
+ it do
24
+ expect(container.pack!).to eql(1)
25
+ expect(container.packed_successfully).to be(true)
26
+ end
27
+ end
28
+
29
+ context 'with items that fit exactly in three packings' do
30
+ before do
31
+ 35.times { container.items << items[0].dup }
32
+ 30.times { container.items << items[1].dup }
33
+ 10.times { container.items << items[2].dup }
34
+ 15.times { container.items << items[3].dup }
35
+ end
36
+
37
+ it do
38
+ expect(container.pack!).to eql(3)
39
+ expect(container.packed_successfully).to be(true)
40
+ end
41
+ end
42
+
43
+ context 'with a packing limit of one and too many items' do
44
+ before do
45
+ container.packings_limit = 1
46
+ 35.times { container.items << items[0].dup }
47
+ 30.times { container.items << items[1].dup }
48
+ 10.times { container.items << items[2].dup }
49
+ 15.times { container.items << items[3].dup }
50
+ end
51
+
52
+ it do
53
+ expect(container.pack!).to eql(0)
54
+ expect(container.packed_successfully).to be(false)
55
+ end
56
+ end
57
+
58
+ context 'with random container and random items' do
59
+ let(:dimensions) { [x, y, z] }
60
+ let(:x) { rand(1..75) }
61
+ let(:y) { rand(1..75) }
62
+ let(:z) { rand(1..75) }
63
+
64
+ let(:items) do
65
+ (1..rand(1..100)).map do
66
+ Item.new([x / rand(1..5), y / rand(1..5), z / rand(1..5)])
67
+ end
68
+ end
69
+
70
+ before do
71
+ container.items = items
72
+ container.pack!
73
+ end
74
+
75
+ it { expect(container.packed_successfully).to be(true) }
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,73 @@
1
+ module BoxPacker
2
+ describe Packing do
3
+ subject(:packing) { Packing.new(total_volume, total_weight) }
4
+ let(:total_volume) { 200 }
5
+ let(:total_weight) { 50 }
6
+ let(:items) {[
7
+ Item.new([2, 4, 1], weight: 5),
8
+ Item.new([5, 2, 7], weight: 6),
9
+ Item.new([8, 4, 2], weight: 3)
10
+ ]}
11
+
12
+ it 'defaults to empty' do
13
+ expect(packing).to eql([])
14
+ end
15
+
16
+ describe '#<<' do
17
+ before { items.each{|i| packing << i} }
18
+
19
+ it { expect(packing.remaining_volume).to eql(58) }
20
+ it { expect(packing.remaining_weight).to eql(36) }
21
+
22
+ context 'with items that do not have weight' do
23
+ let(:items) {[
24
+ Item.new([2, 4, 1]),
25
+ Item.new([5, 2, 7]),
26
+ Item.new([8, 4, 2])
27
+ ]}
28
+
29
+ it { expect(packing.remaining_weight).to be(50) }
30
+ end
31
+
32
+ context 'with total_weight nil' do
33
+ let(:total_weight) { nil }
34
+ it { expect(packing.remaining_weight).to be(nil) }
35
+ end
36
+ end
37
+
38
+ describe '#fit?' do
39
+ before { items.each{|i| packing << i} }
40
+
41
+ context 'with item that fits' do
42
+ let(:item) { Item.new([1, 5, 5], weight: 5)}
43
+ it { expect(packing.fit?(item)).to be(true) }
44
+ end
45
+
46
+ context 'with item thats already packed' do
47
+ it { expect(packing.fit?(items[0])).to be(false) }
48
+ end
49
+
50
+ context 'with item thats too large' do
51
+ let(:item) { Item.new([3, 5, 5], weight: 25)}
52
+ it { expect(packing.fit?(item)).to be(false) }
53
+ end
54
+
55
+ context 'with item thats too heavy' do
56
+ let(:item) { Item.new([1, 5, 5], weight: 45)}
57
+ it { expect(packing.fit?(item)).to be(false) }
58
+ end
59
+
60
+ context 'with total_weight nil and item that fits' do
61
+ let(:total_weight) { nil }
62
+ let(:item) { Item.new([1, 5, 5], weight: 5)}
63
+ it { expect(packing.fit?(item)).to be(true) }
64
+ end
65
+
66
+ context 'with item that has no weight but fits' do
67
+ let(:item) { Item.new([1, 5, 5])}
68
+ it { expect(packing.fit?(item)).to be(true) }
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ RSpec.configure do |config|
2
+
3
+ config.expect_with :rspec do |expectations|
4
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
5
+ end
6
+
7
+ config.mock_with :rspec do |mocks|
8
+ mocks.verify_partial_doubles = true
9
+ end
10
+
11
+ require_relative '../lib/box_packer'
12
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
13
+ config.include BoxPacker::Matchers
14
+ end
@@ -0,0 +1,40 @@
1
+ require 'attr_extras'
2
+ require 'rspec/matchers/built_in/yield'
3
+
4
+ module BoxPacker
5
+ module Matchers
6
+ class YieldEachOnce
7
+ pattr_initialize :expected
8
+
9
+ def matches?(block)
10
+ @probe = RSpec::Matchers::BuiltIn::YieldProbe.probe(block)
11
+ @actual = @probe.successive_yield_args
12
+
13
+ @actual.each do |value|
14
+ unless expected.delete(value)
15
+ @failure_value = value
16
+ return false
17
+ end
18
+ end
19
+ true
20
+ end
21
+
22
+ def supports_block_expectations?
23
+ true
24
+ end
25
+
26
+ def description
27
+ 'be in expected array'
28
+ end
29
+
30
+ def failure_message
31
+ "value #{@failure_value.to_a} was not in expected array"
32
+ end
33
+
34
+ end
35
+
36
+ def yield_each_once(expected)
37
+ YieldEachOnce.new(expected)
38
+ end
39
+ end
40
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: box_packer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-23 00:00:00.000000000 Z
11
+ date: 2014-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -28,14 +28,42 @@ dependencies:
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: attr_extras
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
41
69
  description: A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit
@@ -46,14 +74,30 @@ extensions: []
46
74
  extra_rdoc_files: []
47
75
  files:
48
76
  - .gitignore
77
+ - .rspec
49
78
  - Gemfile
50
79
  - LICENSE.txt
51
80
  - README.md
52
81
  - Rakefile
53
82
  - box_packer.gemspec
54
83
  - lib/box_packer.rb
84
+ - lib/box_packer/box.rb
85
+ - lib/box_packer/builder.rb
86
+ - lib/box_packer/container.rb
87
+ - lib/box_packer/dimensions.rb
88
+ - lib/box_packer/item.rb
89
+ - lib/box_packer/packer.rb
90
+ - lib/box_packer/packing.rb
91
+ - lib/box_packer/position.rb
55
92
  - lib/box_packer/version.rb
56
- - lib/tc_box_packer.rb
93
+ - spec/lib/box_spec.rb
94
+ - spec/lib/container_spec.rb
95
+ - spec/lib/dimensions_spec.rb
96
+ - spec/lib/item_spec.rb
97
+ - spec/lib/packer_spec.rb
98
+ - spec/lib/packing_spec.rb
99
+ - spec/spec_helper.rb
100
+ - spec/support/matchers/box_packer.rb
57
101
  homepage: ''
58
102
  licenses:
59
103
  - MIT
@@ -64,18 +108,26 @@ require_paths:
64
108
  - lib
65
109
  required_ruby_version: !ruby/object:Gem::Requirement
66
110
  requirements:
67
- - - ! '>='
111
+ - - '>='
68
112
  - !ruby/object:Gem::Version
69
113
  version: '0'
70
114
  required_rubygems_version: !ruby/object:Gem::Requirement
71
115
  requirements:
72
- - - ! '>='
116
+ - - '>='
73
117
  - !ruby/object:Gem::Version
74
118
  version: '0'
75
119
  requirements: []
76
120
  rubyforge_project:
77
- rubygems_version: 2.2.1
121
+ rubygems_version: 2.4.1
78
122
  signing_key:
79
123
  specification_version: 4
80
124
  summary: A Heuristic First-Fit 3D Bin Packing Algorithm with Weight Limit
81
- test_files: []
125
+ test_files:
126
+ - spec/lib/box_spec.rb
127
+ - spec/lib/container_spec.rb
128
+ - spec/lib/dimensions_spec.rb
129
+ - spec/lib/item_spec.rb
130
+ - spec/lib/packer_spec.rb
131
+ - spec/lib/packing_spec.rb
132
+ - spec/spec_helper.rb
133
+ - spec/support/matchers/box_packer.rb
@@ -1,171 +0,0 @@
1
- require_relative "box_packer"
2
- require "test/unit"
3
- require 'benchmark'
4
-
5
- class TestBoxPacker < Test::Unit::TestCase
6
-
7
- def setup
8
- @containers = []
9
- @containers[0] = BoxPacker::Container.new("A",[10, 1, 1],15)
10
- @containers[1] = BoxPacker::Container.new("B",[1, 10, 5],50)
11
- @containers[2] = BoxPacker::Container.new("C",[10, 15, 5],325)
12
-
13
- @items = []
14
- @items[0] = BoxPacker::Item.new("a",[1, 1, 1],1)
15
- @items[1] = BoxPacker::Item.new("b",[3, 1, 1],6)
16
- @items[2] = BoxPacker::Item.new("c",[5, 5, 1],15)
17
- @items[3] = BoxPacker::Item.new("d",[5, 5, 5],20)
18
-
19
- @skip_items_count = false
20
- end
21
-
22
- def teardown
23
- @containers.delete_if{ |container| container.packings.empty? }
24
- unless @containers.empty?
25
- assert(@containers[0].packings.map(&:count).reduce(:+) == @containers[0].items.count, "Packed items less than container's list") unless @skip_items_count
26
- assert(@containers[0].packings.map(&:remaining_weight).all? { |rw| rw >= 0 }, "Packing too heavy")
27
- end
28
- end
29
-
30
- def test_no_items
31
- assert_nil(@containers[0].pack)
32
- end
33
-
34
- def test_items_too_heavy
35
- @items[0].weight = 16
36
- @containers[0].items << @items[0]
37
- assert_nil(@containers[0].pack)
38
- end
39
-
40
- def test_items_too_big
41
- @containers[0].items << @items[2]
42
- assert_nil(@containers[0].pack)
43
- end
44
-
45
- def test_too_many_packings
46
- @containers[0].packings_limit = 1
47
- 11.times { @containers[0].items << @items[0] }
48
- assert_nil(@containers[0].pack)
49
- @skip_items_count = true
50
- end
51
-
52
- def test_1d_one_full_packing
53
- 7.times { @containers[0].items << @items[0] }
54
- @containers[0].items << @items[1]
55
- assert_equal(1, @containers[0].pack)
56
- assert_equal(0, @containers[0].packings[0].remaining_volume)
57
- end
58
-
59
- def test_1d_two_packings_due_to_weight_limit
60
- 3.times { @containers[0].items << @items[1] }
61
- assert_equal(2, @containers[0].pack)
62
- end
63
-
64
- def test_1d_three_full_packings
65
- @containers[0].packings_limit = 4
66
- 3.times { @containers[0].items << @items[0] << @items[1] }
67
- 18.times { @containers[0].items << @items[0] }
68
- assert_equal(3, @containers[0].pack)
69
- end
70
-
71
- def test_2d_one_full_packing_two_identical_items
72
- 2.times { @containers[1].items << @items[2] }
73
- assert_equal(1, @containers[1].pack)
74
- assert_equal(0, @containers[1].packings[0].remaining_volume)
75
- end
76
-
77
- def test_2d_one_full_packing_multiple_items
78
- 4.times { @containers[1].items << @items[0] }
79
- 7.times { @containers[1].items << @items[1] }
80
- @containers[1].items << @items[2]
81
- check_pack_in_between(@containers[1], 1, 2)
82
- end
83
-
84
- def test_2d_three_full_packings
85
- @containers[1].packings_limit = 5
86
- 8.times { @containers[1].items << @items[0] }
87
- 14.times { @containers[1].items << @items[1] }
88
- 4.times { @containers[1].items << @items[2] }
89
- check_pack_in_between(@containers[1], 3, 4)
90
- end
91
-
92
- def test_3d_one_full_packing_with_identical_items
93
- 6.times { @containers[2].items << @items[3] }
94
- assert_equal(1, @containers[2].pack)
95
- assert_equal(0, @containers[2].packings[0].remaining_volume)
96
- end
97
-
98
- def test_3d_one_full_packing_with_multiple_items
99
- 20.times { @containers[2].items << @items[0] }
100
- 10.times { @containers[2].items << @items[1] }
101
- 8.times { @containers[2].items << @items[2] }
102
- 4.times { @containers[2].items << @items[3] }
103
- assert_equal(1, @containers[2].pack)
104
- assert_equal(0, @containers[2].packings[0].remaining_volume)
105
- end
106
-
107
- def test_3d_three_full_packings
108
- @containers[2].packings_limit = 5
109
- 35.times { @containers[2].items << @items[0] }
110
- 30.times { @containers[2].items << @items[1] }
111
- 10.times { @containers[2].items << @items[2] }
112
- 15.times { @containers[2].items << @items[3] }
113
- check_pack_in_between(@containers[2], 3, 4)
114
- end
115
-
116
- def test_benchmark
117
- puts "\n\nBenchmarking\n============"
118
- iterations = 500
119
- containers = []
120
-
121
- (1..iterations).each do |i|
122
- container_dimensions = [1 + rand(100), 1 + rand(50), 1 + rand(10)]
123
- container_weight_limit = 1 + rand(1000)
124
- container = BoxPacker::Container.new("c#{i}", container_dimensions, container_weight_limit)
125
- container.packings_limit = 50
126
-
127
- (1..(1+rand(40))).each do |j|
128
- item_dimensions = container_dimensions.map { |c_dimension| 1 + rand(c_dimension) / (1 + rand(5)) }
129
- item_weight = 1 + rand(container_weight_limit / (1 + rand(10)))
130
- container.items << BoxPacker::Item.new("i#{j}", item_dimensions, item_weight)
131
- end
132
- containers << container
133
- end
134
-
135
- Benchmark.bm(15) do |bm|
136
- bm.report('Approx packings') do
137
- containers.each{ |container| container.pack }
138
- end
139
- bm.report('Volume') do
140
- containers.each{ |container| container.pack(:sort_by_volume) }
141
- end
142
- bm.report('Shuffled') do
143
- containers.each{ |container| container.pack(:sort_by_shuffle) }
144
- end
145
- end
146
-
147
- puts "\nBenchmark data\n=============="
148
- puts "iterations: #{iterations}"
149
- benchmark_stats = Hash.new{0}
150
- containers.each do |container|
151
- benchmark_stats[:avg_container_volume] += container.volume
152
- benchmark_stats[:avg_packings_count] += container.packings.count
153
- benchmark_stats[:avg_items_count] += container.items.count
154
- benchmark_stats[:avg_item_volume] += container.items.map(&:volume).reduce(:+) / container.items.count.to_f
155
- end
156
- benchmark_stats.each do |k, v|
157
- benchmark_stats[k] = v / containers.count.to_f
158
- puts "#{k}: #{benchmark_stats[k]}"
159
- end
160
- puts "\n"
161
- end
162
-
163
- private
164
-
165
- def check_pack_in_between(container, lower_bound, upper_bound)
166
- assert(container.pack.between?(lower_bound, upper_bound), "Did not pack into #{lower_bound} (or #{upper_bound}) packings")
167
- assert(container.packings.map(&:remaining_volume).reduce(:+) % container.volume == 0 , "Total remaining volume doesn't add up")
168
- end
169
-
170
- end
171
-