line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Box::Calc; |
2
|
|
|
|
|
|
|
$Box::Calc::VERSION = '1.0200'; |
3
|
7
|
|
|
7
|
|
41619
|
use strict; |
|
7
|
|
|
|
|
13
|
|
|
7
|
|
|
|
|
166
|
|
4
|
7
|
|
|
7
|
|
4356
|
use Moose; |
|
7
|
|
|
|
|
2705066
|
|
|
7
|
|
|
|
|
54
|
|
5
|
7
|
|
|
7
|
|
51508
|
use Box::Calc::BoxType; |
|
7
|
|
|
|
|
98572
|
|
|
7
|
|
|
|
|
310
|
|
6
|
7
|
|
|
7
|
|
4552
|
use Box::Calc::Item; |
|
7
|
|
|
|
|
1937
|
|
|
7
|
|
|
|
|
245
|
|
7
|
7
|
|
|
7
|
|
4402
|
use Box::Calc::Box; |
|
7
|
|
|
|
|
9859
|
|
|
7
|
|
|
|
|
298
|
|
8
|
7
|
|
|
7
|
|
62
|
use List::MoreUtils qw(natatime); |
|
7
|
|
|
|
|
14
|
|
|
7
|
|
|
|
|
115
|
|
9
|
7
|
|
|
7
|
|
3111
|
use List::Util qw(max); |
|
7
|
|
|
|
|
13
|
|
|
7
|
|
|
|
|
492
|
|
10
|
7
|
|
|
7
|
|
33
|
use Ouch; |
|
7
|
|
|
|
|
16
|
|
|
7
|
|
|
|
|
479
|
|
11
|
7
|
|
|
7
|
|
35
|
use Log::Any qw($log); |
|
7
|
|
|
|
|
14
|
|
|
7
|
|
|
|
|
59
|
|
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
=head1 NAME |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
Box::Calc - Packing Algorithm |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
=head1 VERSION |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
version 1.0200 |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
=head1 SYNOPSIS |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
use Box::Calc; |
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
my $box_calc = Box::Calc->new; |
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
# define the possible box types |
28
|
|
|
|
|
|
|
$box_calc->add_box_type( x => 12, y => 12, z => 18, weight => 16, name => 'big box' ); |
29
|
|
|
|
|
|
|
$box_calc->add_box_type( x => 4, y => 6, z => 8, weight => 6, name => 'small box' ); |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# define the items you want to put into boxes |
32
|
|
|
|
|
|
|
$box_calc->add_item( 3, { x => 6, y => 3, z => 3, weight => 12, name => 'soda' }); |
33
|
|
|
|
|
|
|
$box_calc->add_item( 1, { x => 3.3, y => 3, z => 4, weight => 4.5, name => 'apple' }); |
34
|
|
|
|
|
|
|
$box_calc->add_item( 2, { x => 8, y => 2.5, z => 2.5, weight => 14, name => 'water bottle' }); |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
# figure out what you need to pack this stuff |
37
|
|
|
|
|
|
|
$box_calc->pack_items; |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
# how many boxes do you need |
40
|
|
|
|
|
|
|
my $box_count = $box_calc->count_boxes; # 2 |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
# interrogate the boxes |
43
|
|
|
|
|
|
|
my $box = $box_calc->get_box(-1); # the last box |
44
|
|
|
|
|
|
|
my $weight = $box->calculate_weight; |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
# get a packing list |
47
|
|
|
|
|
|
|
my $packing_list = $box_calc->packing_list; |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
=head1 DESCRIPTION |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
Box::Calc helps you determine what can fit into a box for shipping or storage purposes. It will try to use the smallest box possible of the box types. If every item won't fit into your largest box, then it will span the boxes letting you know how many boxes you'll need. |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
Once it's done packing the boxes, you can get a packing list for each box, as well as the weight of each box. |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
=head2 How The Algorithm Works |
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
Box::Calc is intended to pack boxes in the simplest way possible. Here's what it does: |
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
=over |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
=item 1 |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
Sort all the items by volume. |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
=item 2 |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
Eliminate all boxes that won't fit the largest items. |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
=item 3 |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
Choose the smallest box still available. |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
=item 4 |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
Place the items in a row starting with the largest items. |
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
=item 5 |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
When the row runs out of space, add another. |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
=item 6 |
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
When you run out of space to add rows, add a layer. |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
=item 7 |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
When you run out of layers either start over with a bigger box, or if there are no bigger boxes span to a second box. |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
=item 8 |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
Repeat from step 3 until all items are packed into boxes. |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
=back |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
=head2 Motivation |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
At The Game Crafter (L<http://www.thegamecrafter.com>) we ship a lot of games and game pieces. We tried using a more complicated system for figuring out which size box to use, or how many boxes would be needed in a spanning situation. The problem was that those algorithms made the boxes pack so tightly that our staff spent a lot more time putting the boxes together. This algorithm is relatively dumb, but dumb in a good way. The boxes are easy and fast to pack. By releasing this, we hope it can help those who are either using too complicated a system, or no system at all for figuring out how many boxes they need for shipping/storing materials. |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
=head2 Tips |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
When adding items, be sure to use the outer most dimensions of oddly shaped items, otherwise they may not fit the box. |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
When adding box types, be sure to use the inside dimensions of the box. If you plan to line the box with padding, then subtract the padding from the dimensions, and also add the padding to the weight of the box. |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
What units you use (inches, centimeters, ounces, pounds, grams, kilograms, etc) don't matter as long as you use them consistently. |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
=head1 METHODS |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
=head2 new() |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
Constructor. |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
=head2 box_types() |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
Returns an array reference of the L<Box::Calc::BoxType>s registered. |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
=head2 count_box_types() |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
Returns the number of L<Box::Calc::BoxType>s registered. |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
=head2 get_box_type(index) |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
Returns a specific L<Box::Calc::BoxType> from the list of C<box_types> |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
=over |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
=item index |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
An array index. For example this would return the last box type added: |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
$box_calc->get_box_type(-1) |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
=back |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
=cut |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
has box_types => ( |
138
|
|
|
|
|
|
|
is => 'rw', |
139
|
|
|
|
|
|
|
isa => 'ArrayRef[Box::Calc::BoxType]', |
140
|
|
|
|
|
|
|
default => sub { [] }, |
141
|
|
|
|
|
|
|
traits => ['Array'], |
142
|
|
|
|
|
|
|
handles => { |
143
|
|
|
|
|
|
|
push_box_types => 'push', |
144
|
|
|
|
|
|
|
count_box_types => 'count', |
145
|
|
|
|
|
|
|
get_box_type => 'get', |
146
|
|
|
|
|
|
|
} |
147
|
|
|
|
|
|
|
); |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
=head2 add_box_type(params) |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
Adds a new L<Box::Calc::BoxType> to the list of C<box_types>. Returns the newly created L<Box::Calc::BoxType> instance. |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
=over |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
=item params |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
The list of constructor parameters for L<Box::Calc::BoxType>. |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
B<NOTE:> You can optionally include an argument of "categories" and a box type will be created for each category so you don't have to do it manually. |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
=back |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
=cut |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
sub add_box_type { |
166
|
28
|
|
|
28
|
1
|
1951
|
my $self = shift; |
167
|
28
|
|
|
|
|
38
|
my $args; |
168
|
28
|
100
|
|
|
|
75
|
if (ref $_[0] eq 'HASH') { |
169
|
10
|
|
|
|
|
15
|
$args = shift; |
170
|
|
|
|
|
|
|
} |
171
|
|
|
|
|
|
|
else { |
172
|
18
|
|
|
|
|
76
|
$args = { @_ }; |
173
|
|
|
|
|
|
|
} |
174
|
28
|
|
|
|
|
55
|
my $categories = delete $args->{categories}; |
175
|
28
|
100
|
|
|
|
68
|
if (defined $categories) { |
176
|
3
|
|
|
|
|
14
|
foreach my $category (@{$categories}) { |
|
3
|
|
|
|
|
8
|
|
177
|
4
|
|
|
|
|
7
|
my %copy = %{$args}; |
|
4
|
|
|
|
|
22
|
|
178
|
4
|
|
|
|
|
12
|
$copy{category} = $category; |
179
|
4
|
|
|
|
|
181
|
$self->push_box_types(Box::Calc::BoxType->new(%copy)); |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
} |
182
|
|
|
|
|
|
|
else { |
183
|
25
|
|
|
|
|
808
|
$self->push_box_types(Box::Calc::BoxType->new($args)); |
184
|
|
|
|
|
|
|
} |
185
|
28
|
|
|
|
|
1049
|
return $self->get_box_type(-1); |
186
|
|
|
|
|
|
|
} |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
=head2 box_type_categories() |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
Returns an array reference of categories associated with the box types. |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
=cut |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
has box_type_categories => ( |
196
|
|
|
|
|
|
|
is => 'rw', |
197
|
|
|
|
|
|
|
lazy => 1, |
198
|
|
|
|
|
|
|
isa => 'ArrayRef', |
199
|
|
|
|
|
|
|
default => sub { |
200
|
|
|
|
|
|
|
my $self = shift; |
201
|
|
|
|
|
|
|
my %categories = (); |
202
|
|
|
|
|
|
|
foreach my $box_type (@{$self->box_types}) { |
203
|
|
|
|
|
|
|
next if $box_type->category eq ''; |
204
|
|
|
|
|
|
|
$categories{$box_type->category} = 1; |
205
|
|
|
|
|
|
|
} |
206
|
|
|
|
|
|
|
return [sort keys %categories]; |
207
|
|
|
|
|
|
|
}, |
208
|
|
|
|
|
|
|
); |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
=head2 sort_box_types_by_volume() |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
Sorts the list of C<box_types> by volume and then returns an array reference of that list. |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=over |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
=item types |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
Optional. Array ref of box types. Will call C<box_types> if not passed in. |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
=back |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
=cut |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
sub sort_box_types_by_volume { |
226
|
17
|
|
|
17
|
1
|
34
|
my $self = shift; |
227
|
17
|
|
33
|
|
|
570
|
my $types = shift || $self->box_types; |
228
|
17
|
|
|
|
|
45
|
my @sorted = sort { ($a->volume) <=> ($b->volume ) } @{$types}; |
|
61
|
|
|
|
|
1924
|
|
|
17
|
|
|
|
|
55
|
|
229
|
17
|
|
|
|
|
64
|
return \@sorted; |
230
|
|
|
|
|
|
|
} |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
=head2 determine_viable_box_types(category) |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
Given the list of C<items> and the list of C<box_types> this method rules out box types that cannot hold the largest item, and returns the list of box types that will work sorted by volume. |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
=over |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
=item category |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
Optional. If this is specified, it will match this category name to the categories attached to the boxes and only provide a list of boxes that match that category. |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
=back |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
=cut |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
sub determine_viable_box_types { |
247
|
16
|
|
|
16
|
1
|
1627
|
my ($self, $category) = @_; |
248
|
16
|
|
|
|
|
28
|
my ($item_x, $item_y, $item_z) = sort {$b <=> $a} @{$self->find_max_dimensions_of_items}; |
|
48
|
|
|
|
|
85
|
|
|
16
|
|
|
|
|
600
|
|
249
|
16
|
|
|
|
|
33
|
my @viable; |
250
|
16
|
|
|
|
|
24
|
foreach my $box_type (@{$self->sort_box_types_by_volume}) { |
|
16
|
|
|
|
|
59
|
|
251
|
52
|
100
|
|
|
|
123
|
if (defined $category) { |
252
|
18
|
100
|
|
|
|
524
|
next unless $category eq $box_type->category; |
253
|
|
|
|
|
|
|
} |
254
|
39
|
|
|
|
|
47
|
my ($box_type_x, $box_type_y, $box_type_z) = @{$box_type->dimensions}; |
|
39
|
|
|
|
|
2280
|
|
255
|
39
|
50
|
66
|
|
|
265
|
if ($item_x <= $box_type_x && $item_y <= $box_type_y && $item_z <= $box_type_z) { |
|
|
|
66
|
|
|
|
|
256
|
33
|
|
|
|
|
75
|
push @viable, $box_type; |
257
|
|
|
|
|
|
|
} |
258
|
|
|
|
|
|
|
} |
259
|
16
|
100
|
|
|
|
66
|
unless (scalar @viable) { |
260
|
1
|
|
|
|
|
10
|
$log->fatal('There are no box types that can fit the items.'); |
261
|
1
|
|
|
|
|
52
|
ouch 'no viable box types', 'There are no box types that can fit the items. ('.join(', ', $item_x, $item_y, $item_z).')', [$item_x, $item_y, $item_z]; |
262
|
|
|
|
|
|
|
} |
263
|
15
|
|
|
|
|
66
|
return \@viable; |
264
|
|
|
|
|
|
|
} |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
=head2 items() |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
Returns an array reference of the L<Box::Calc::Item>s registered. |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
=head2 count_items() |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
Returns the number of L<Box::Calc::Item>s registered. |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
=head2 get_item(index) |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
Returns a specific L<Box::Calc::Item>. |
277
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
=over |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
=item index |
281
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
The array index of the item as it was registered. |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
=back |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
=cut |
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
has items => ( |
289
|
|
|
|
|
|
|
is => 'rw', |
290
|
|
|
|
|
|
|
isa => 'ArrayRef[Box::Calc::Item]', |
291
|
|
|
|
|
|
|
default => sub { [] }, |
292
|
|
|
|
|
|
|
traits => ['Array'], |
293
|
|
|
|
|
|
|
handles => { |
294
|
|
|
|
|
|
|
push_items => 'push', |
295
|
|
|
|
|
|
|
count_items => 'count', |
296
|
|
|
|
|
|
|
get_item => 'get', |
297
|
|
|
|
|
|
|
} |
298
|
|
|
|
|
|
|
); |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
=head2 add_item(quantity, params) |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
Registers a new item. Returns the new item registered. |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
=over |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
=item quantity |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
How many copies of this item should be included in the package? |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=item params |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
The constructor parameters for the L<Box::Calc::Item>. |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
=back |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
=cut |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
sub add_item { |
319
|
26
|
|
|
26
|
1
|
11662
|
my ($self, $quantity, @params) = @_; |
320
|
26
|
|
|
|
|
889
|
my $item = Box::Calc::Item->new(@params); |
321
|
26
|
|
|
|
|
78
|
for (1..$quantity) { |
322
|
10589
|
|
|
|
|
367584
|
$self->push_items($item); |
323
|
|
|
|
|
|
|
} |
324
|
26
|
|
|
|
|
960
|
return $self->get_item(-1); |
325
|
|
|
|
|
|
|
} |
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
=head2 load(payload) |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
Allows the loading of an entire dataset. |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
=over |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
=item payload |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
A hash reference containing the output of the C<dump> method, with two exceptions: |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
=over |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
=item * |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
You can create a C<categories> element that is an array ref in each box type rather than creating duplicate box types for each category. |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
=item * |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
You can create a C<quantity> element in each item rather than creating duplicate items to represent the quantity. |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
=back |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
=back |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
=cut |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
sub load { |
354
|
1
|
|
|
1
|
1
|
7
|
my ($self, $payload) = @_; |
355
|
|
|
|
|
|
|
# note that we copy the box type and item to avoid modifying the original |
356
|
1
|
|
|
|
|
2
|
foreach my $type (@{$payload->{box_types}}) { |
|
1
|
|
|
|
|
3
|
|
357
|
7
|
|
|
|
|
8
|
$self->add_box_type(%{$type}); |
|
7
|
|
|
|
|
25
|
|
358
|
|
|
|
|
|
|
} |
359
|
1
|
|
|
|
|
2
|
foreach my $item (@{$payload->{items}}) { |
|
1
|
|
|
|
|
49
|
|
360
|
6
|
|
50
|
|
|
22
|
$self->add_item($item->{quantity} || 1, %{$item}); |
|
6
|
|
|
|
|
25
|
|
361
|
|
|
|
|
|
|
} |
362
|
|
|
|
|
|
|
} |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
=head2 dump() |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
=cut |
367
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
sub dump { |
369
|
2
|
|
|
2
|
1
|
6
|
my ($self) = @_; |
370
|
2
|
|
|
|
|
4
|
my $payload = {}; |
371
|
2
|
|
|
|
|
5
|
foreach my $type (@{$self->box_types}) { |
|
2
|
|
|
|
|
57
|
|
372
|
14
|
|
|
|
|
15
|
push @{$payload->{box_types}}, $type->describe; |
|
14
|
|
|
|
|
50
|
|
373
|
|
|
|
|
|
|
} |
374
|
2
|
|
|
|
|
5
|
foreach my $item (@{$self->items}) { |
|
2
|
|
|
|
|
55
|
|
375
|
12
|
|
|
|
|
15
|
push @{$payload->{items}}, $item->describe; |
|
12
|
|
|
|
|
45
|
|
376
|
|
|
|
|
|
|
} |
377
|
2
|
|
|
|
|
7
|
return $payload; |
378
|
|
|
|
|
|
|
} |
379
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
=head2 sort_items_by_volume() |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
Returns an array reference of the list of C<items> registered sorted by volume. |
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
=over |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
=item items |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
Optional. An array reference of items. Will call C<items> if not passed in. |
390
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
=back |
392
|
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
=cut |
394
|
|
|
|
|
|
|
|
395
|
|
|
|
|
|
|
sub sort_items_by_volume { |
396
|
1
|
|
|
1
|
1
|
4
|
my $self = shift; |
397
|
1
|
|
33
|
|
|
49
|
my $items = shift || $self->items; |
398
|
1
|
|
|
|
|
4
|
my @sorted = sort { ($a->volume) <=> ($b->volume ) } @{$items}; |
|
10
|
|
|
|
|
372
|
|
|
1
|
|
|
|
|
6
|
|
399
|
1
|
|
|
|
|
6
|
return \@sorted; |
400
|
|
|
|
|
|
|
} |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
=head2 sort_items_by_zxy() |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
Returns an array reference of the list of C<items> registered sorted by z, then x, then y, ascending. |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
=over |
407
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
=item items |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
Optional. An array reference of items. Will call C<items> if not passed in. |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
=back |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
=cut |
415
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
sub sort_items_by_zxy { |
417
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
418
|
0
|
|
0
|
|
|
0
|
my $items = shift || $self->items; |
419
|
|
|
|
|
|
|
my @sorted = sort { |
420
|
0
|
0
|
0
|
|
|
0
|
$a->z <=> $b->z |
421
|
|
|
|
|
|
|
|| $a->x <=> $b->x |
422
|
|
|
|
|
|
|
|| $a->y <=> $b->y |
423
|
0
|
|
|
|
|
0
|
} @{$items}; |
|
0
|
|
|
|
|
0
|
|
424
|
0
|
|
|
|
|
0
|
return \@sorted; |
425
|
|
|
|
|
|
|
} |
426
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
=head2 sort_items_by_z_desc_A() |
428
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
Returns an array reference of the list of C<items> registered sorted by z DESC, then area DESC |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
=over |
432
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
=item items |
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
Optional. An array reference of items. Will call C<items> if not passed in. |
436
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
=back |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
=cut |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
sub sort_items_by_z_desc_A { |
442
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
443
|
0
|
|
0
|
|
|
0
|
my $items = shift || $self->items; |
444
|
0
|
|
|
|
|
0
|
my @sorted = map { $_->[1] } |
445
|
|
|
|
|
|
|
sort { |
446
|
|
|
|
|
|
|
$b->[0]->{z} <=> $a->[0]->{z} |
447
|
|
|
|
|
|
|
|| $b->[0]->{A} <=> $a->[0]->{A} |
448
|
0
|
0
|
|
|
|
0
|
} |
449
|
|
|
|
|
|
|
##Fetch Z and calculate A |
450
|
0
|
|
|
|
|
0
|
map { [ { z=>$_->z, A=>$_->x*$_->y }, $_ ] } @{$items}; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
451
|
0
|
|
|
|
|
0
|
return \@sorted; |
452
|
|
|
|
|
|
|
} |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
=head2 sort_items_by_zA() |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
Returns an array reference of the list of C<items> registered sorted by z ASC, then area DESC |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
=over |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
=item items |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
Optional. An array reference of items. Will call C<items> if not passed in. |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
=back |
465
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
=cut |
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
sub sort_items_by_zA { |
469
|
10
|
|
|
10
|
1
|
22
|
my $self = shift; |
470
|
10
|
|
66
|
|
|
302
|
my $items = shift || $self->items; |
471
|
10552
|
|
|
|
|
13539
|
my @sorted = map { $_->[1] } |
472
|
|
|
|
|
|
|
sort { |
473
|
|
|
|
|
|
|
$a->[0]->{z} <=> $b->[0]->{z} |
474
|
|
|
|
|
|
|
|| $b->[0]->{A} <=> $a->[0]->{A} |
475
|
10642
|
50
|
|
|
|
23685
|
} |
476
|
|
|
|
|
|
|
##Fetch Z and calculate A |
477
|
10
|
|
|
|
|
20
|
map { [ { z=>$_->z, A=>$_->x*$_->y }, $_ ] } @{$items}; |
|
10552
|
|
|
|
|
310291
|
|
|
10
|
|
|
|
|
150
|
|
478
|
10
|
|
|
|
|
4216
|
return \@sorted; |
479
|
|
|
|
|
|
|
} |
480
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
=head2 sort_items_by_Az() |
482
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
=over |
484
|
|
|
|
|
|
|
|
485
|
|
|
|
|
|
|
=item items |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
Optional. An array reference of items. Will call C<items> if not passed in. |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
=back |
490
|
|
|
|
|
|
|
|
491
|
|
|
|
|
|
|
Returns an array reference of the list of C<items> registered sorted by A DESC, then z ASC |
492
|
|
|
|
|
|
|
|
493
|
|
|
|
|
|
|
=cut |
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
sub sort_items_by_Az { |
496
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
497
|
0
|
|
0
|
|
|
0
|
my $items = shift || $self->items; |
498
|
0
|
|
|
|
|
0
|
my @sorted = map { $_->[1] } |
499
|
|
|
|
|
|
|
sort { |
500
|
|
|
|
|
|
|
$b->[0]->{A} <=> $a->[0]->{A} |
501
|
|
|
|
|
|
|
|| $a->[0]->{z} <=> $b->[0]->{z} |
502
|
0
|
0
|
|
|
|
0
|
} |
503
|
|
|
|
|
|
|
##Fetch Z and calculate A |
504
|
0
|
|
|
|
|
0
|
map { [ { z=>$_->z, A=>$_->x*$_->y }, $_ ] } @{$items}; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
505
|
0
|
|
|
|
|
0
|
return \@sorted; |
506
|
|
|
|
|
|
|
} |
507
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
=head2 find_max_dimensions_of_items() |
509
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
Given the registered C<items>, returns the max C<x>, C<y>, and C<z> of all items registered as an array reference. |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
=cut |
513
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
has find_max_dimensions_of_items => ( |
515
|
|
|
|
|
|
|
is => 'rw', |
516
|
|
|
|
|
|
|
lazy => 1, |
517
|
|
|
|
|
|
|
isa => 'ArrayRef', |
518
|
|
|
|
|
|
|
clearer => 'clear_max_dimensions_of_items', |
519
|
|
|
|
|
|
|
default => sub { |
520
|
|
|
|
|
|
|
my $self = shift; |
521
|
|
|
|
|
|
|
my $x = 0; |
522
|
|
|
|
|
|
|
my $y = 0; |
523
|
|
|
|
|
|
|
my $z = 0; |
524
|
|
|
|
|
|
|
foreach my $item (@{$self->items}) { |
525
|
|
|
|
|
|
|
my ($ex, $ey, $ez) = @{$item->dimensions}; |
526
|
|
|
|
|
|
|
$x = $ex if $ex > $x; |
527
|
|
|
|
|
|
|
$y = $ey if $ey > $y; |
528
|
|
|
|
|
|
|
$z = $ez if $ez > $z; |
529
|
|
|
|
|
|
|
} |
530
|
|
|
|
|
|
|
return [$x, $y, $z]; |
531
|
|
|
|
|
|
|
} |
532
|
|
|
|
|
|
|
); |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
=head2 boxes() |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
Returns an array reference of the list of L<Box::Calc::Box>es needed to pack up the items. |
537
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
B<NOTE:> This will be empty until you call C<pack_items>. |
539
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
=head2 count_boxes() |
541
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
Returns the number of boxes needed to pack up the items. |
543
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
=head2 get_box(index) |
545
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
Fetches a specific box from the list of <boxes>. |
547
|
|
|
|
|
|
|
|
548
|
|
|
|
|
|
|
=over |
549
|
|
|
|
|
|
|
|
550
|
|
|
|
|
|
|
=item index |
551
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
The array index of the box you wish to fetc. |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
=back |
555
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
=cut |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
has boxes => ( |
559
|
|
|
|
|
|
|
is => 'rw', |
560
|
|
|
|
|
|
|
isa => 'ArrayRef[Box::Calc::Box]', |
561
|
|
|
|
|
|
|
default => sub { [] }, |
562
|
|
|
|
|
|
|
traits => ['Array'], |
563
|
|
|
|
|
|
|
handles => { |
564
|
|
|
|
|
|
|
push_boxes => 'push', |
565
|
|
|
|
|
|
|
count_boxes => 'count', |
566
|
|
|
|
|
|
|
get_box => 'get', |
567
|
|
|
|
|
|
|
} |
568
|
|
|
|
|
|
|
); |
569
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
=head2 reset_boxes() |
571
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
Deletes the list of C<boxes>. |
573
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
If you wish to rerun the packing you should use this to delete the list of C<boxes> first. This is handy if you needed to add an extra item or extra box type after you already ran C<pack_items>. |
575
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
=cut |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
sub reset_boxes { |
579
|
4
|
|
|
4
|
1
|
71204
|
my $self = shift; |
580
|
4
|
|
|
|
|
144
|
$self->boxes([]); |
581
|
|
|
|
|
|
|
} |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
=head2 reset_items() |
584
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
Deletes the list of C<items>. |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
For the sake of speed you may wish to reuse a L<Box::Calc> instance with the box types already pre-loaded. In that case you'll want to use this method to remove the items you've already registered. You'll probably also want to call C<reset_boxes>. |
588
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
=cut |
590
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
sub reset_items { |
592
|
1
|
|
|
1
|
1
|
2
|
my $self = shift; |
593
|
1
|
|
|
|
|
28
|
$self->items([]); |
594
|
1
|
|
|
|
|
39
|
$self->clear_max_dimensions_of_items; |
595
|
|
|
|
|
|
|
} |
596
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
sub make_box { |
598
|
31
|
|
|
31
|
0
|
72
|
my ($self, $box_type) = @_; |
599
|
31
|
|
|
|
|
1112
|
return Box::Calc::Box->new( |
600
|
|
|
|
|
|
|
swap_xy => 1, |
601
|
|
|
|
|
|
|
mail_service_name => $box_type->mail_service_name, |
602
|
|
|
|
|
|
|
x => $box_type->x, |
603
|
|
|
|
|
|
|
y => $box_type->y, |
604
|
|
|
|
|
|
|
z => $box_type->z, |
605
|
|
|
|
|
|
|
weight => $box_type->weight, |
606
|
|
|
|
|
|
|
max_weight => $box_type->max_weight, |
607
|
|
|
|
|
|
|
name => $box_type->name, |
608
|
|
|
|
|
|
|
outer_x => $box_type->outer_x, |
609
|
|
|
|
|
|
|
outer_y => $box_type->outer_y, |
610
|
|
|
|
|
|
|
outer_z => $box_type->outer_z, |
611
|
|
|
|
|
|
|
); |
612
|
|
|
|
|
|
|
} |
613
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
=head2 find_tallest_z ( [ items ] ) |
615
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
Determines the median of z across all items in the list. |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
=over |
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
=item items |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
An array reference of items. Optional. Defaults to C<items>. |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
=back |
625
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
=cut |
627
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
sub find_tallest_z { |
629
|
3
|
|
|
3
|
1
|
9
|
my $self = shift; |
630
|
3
|
|
66
|
|
|
34
|
my $items = shift || $self->items; |
631
|
3
|
|
|
|
|
4
|
return max map { $_->z } @{$items}; |
|
165
|
|
|
|
|
4439
|
|
|
3
|
|
|
|
|
5
|
|
632
|
|
|
|
|
|
|
} |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
=head2 stack_like_items( options ) |
636
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
Stacks all like-sized items into stacks of C<stack_height> for denser packing. Could be used as an optimizer before running C<pack_items>. |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
=over |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
=item options |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
A hash. |
644
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
=over |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
=item items |
648
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
Optional. If not specified, will be the C<items> list. |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
=item stack_height |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
Optional. If not specified, will be determined by calling C<find_tallest_z>. |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
=back |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
=back |
658
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
=cut |
660
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
sub stack_like_items { |
662
|
2
|
|
|
2
|
1
|
3
|
my ($self, %options) = @_; |
663
|
2
|
|
33
|
|
|
63
|
my $items = $options{items} || $self->items; |
664
|
2
|
|
33
|
|
|
9
|
my $stack_height = $options{stack_height} || $self->find_tallest_z($items); |
665
|
2
|
|
|
|
|
7
|
my %like; |
666
|
2
|
|
|
|
|
2
|
foreach my $item (@{$items}) { |
|
2
|
|
|
|
|
5
|
|
667
|
110
|
|
|
|
|
105
|
push @{$like{$item->extent}}, $item; |
|
110
|
|
|
|
|
3116
|
|
668
|
|
|
|
|
|
|
} |
669
|
2
|
|
|
|
|
3
|
my @stacks; |
670
|
2
|
|
|
|
|
5
|
foreach my $kind (values %like) { |
671
|
6
|
50
|
|
|
|
7
|
if (scalar @{$kind} == 1) { |
|
6
|
|
|
|
|
15
|
|
672
|
0
|
|
|
|
|
0
|
push @stacks, $kind->[0]; |
673
|
|
|
|
|
|
|
} |
674
|
|
|
|
|
|
|
else { |
675
|
6
|
|
50
|
|
|
186
|
my $items_per_stack = int($stack_height / $kind->[0]->z) || 1; |
676
|
6
|
|
|
|
|
10
|
my $iterator = natatime($items_per_stack, @{$kind}); |
|
6
|
|
|
|
|
56
|
|
677
|
6
|
|
|
|
|
41
|
while (my @items = $iterator->()) { |
678
|
12
|
|
|
|
|
15
|
my $count = scalar @items; |
679
|
12
|
100
|
|
|
|
22
|
if ($count == 1) { |
680
|
6
|
|
|
|
|
35
|
push @stacks, $items[0]; |
681
|
|
|
|
|
|
|
} |
682
|
|
|
|
|
|
|
else { |
683
|
6
|
|
|
|
|
8
|
my $item = $items[0]; |
684
|
6
|
|
|
|
|
190
|
push @stacks, Box::Calc::Item->new( |
685
|
|
|
|
|
|
|
x => $item->x, |
686
|
|
|
|
|
|
|
y => $item->y, |
687
|
|
|
|
|
|
|
z => $item->z * $count, |
688
|
|
|
|
|
|
|
weight => $item->weight * $count, |
689
|
|
|
|
|
|
|
name => 'Stack of '.$count.' '.$item->name, |
690
|
|
|
|
|
|
|
no_sort => 1, |
691
|
|
|
|
|
|
|
); |
692
|
|
|
|
|
|
|
} |
693
|
|
|
|
|
|
|
} |
694
|
|
|
|
|
|
|
} |
695
|
|
|
|
|
|
|
} |
696
|
2
|
|
|
|
|
17
|
return \@stacks; |
697
|
|
|
|
|
|
|
} |
698
|
|
|
|
|
|
|
|
699
|
|
|
|
|
|
|
=head2 pack_items(options) |
700
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
Uses the list of C<box_types> and the list of C<items> to create the list of boxes to be packed. This method populates the C<boxes> list. |
702
|
|
|
|
|
|
|
|
703
|
|
|
|
|
|
|
=over |
704
|
|
|
|
|
|
|
|
705
|
|
|
|
|
|
|
=item options |
706
|
|
|
|
|
|
|
|
707
|
|
|
|
|
|
|
A hash. |
708
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
=over |
710
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
=item items |
712
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
Optional. If omitted the items list will be populated with whatever the current B<best> general purpose preprocessed item list is. Currently that is C<sort_items_by_zA>. |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
=item category |
716
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
Optional. If this is specified, it will match this category name to the categories attached to the boxes and only pack in boxes that match that category. |
718
|
|
|
|
|
|
|
|
719
|
|
|
|
|
|
|
=back |
720
|
|
|
|
|
|
|
|
721
|
|
|
|
|
|
|
=back |
722
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
=cut |
724
|
|
|
|
|
|
|
|
725
|
|
|
|
|
|
|
sub pack_items { |
726
|
10
|
|
|
10
|
1
|
480
|
my ($self, %options) = @_; |
727
|
10
|
|
|
|
|
31
|
my $category = $options{category}; |
728
|
10
|
|
66
|
|
|
82
|
my $items = $options{items} || $self->sort_items_by_zA; |
729
|
10
|
|
|
|
|
21
|
my $item_count = scalar(@{$items}); |
|
10
|
|
|
|
|
26
|
|
730
|
10
|
|
|
|
|
13
|
my @box_types = @{$self->determine_viable_box_types($category)}; |
|
10
|
|
|
|
|
49
|
|
731
|
10
|
|
|
|
|
29
|
my $countdown = scalar(@box_types); |
732
|
10
|
|
|
|
|
26
|
BOXTYPE: foreach my $box_type (@box_types) { |
733
|
19
|
|
|
|
|
624
|
$log->info("Box Type: ".$box_type->name); |
734
|
19
|
|
|
|
|
389
|
$countdown--; |
735
|
19
|
|
|
|
|
84
|
my $box = $self->make_box($box_type); |
736
|
19
|
|
|
|
|
44
|
ITEM: foreach my $item (@{$items}) { |
|
19
|
|
|
|
|
53
|
|
737
|
17295
|
|
|
|
|
514754
|
$log->info("Item: ".$item->name); |
738
|
|
|
|
|
|
|
|
739
|
|
|
|
|
|
|
# swap the item's x & y if it will make the item fit tighter |
740
|
17295
|
50
|
33
|
|
|
781044
|
if ($item->x > 0 && $item->y > 0) { |
741
|
17295
|
|
|
|
|
44673
|
$log->debug("Item's dimensions are not 0."); |
742
|
17295
|
50
|
33
|
|
|
715619
|
if ($box->x >= $item->y && $box->y >= $item->x) { # see if the item would still fit in the box if it swapped |
743
|
17295
|
|
|
|
|
43667
|
$log->debug('Item would still fit in the box if we rotated it.'); |
744
|
17295
|
|
|
|
|
703420
|
my $original_x_per_layer = int($box->x / $item->x); |
745
|
17295
|
|
|
|
|
511559
|
my $original_y_per_layer = int($box->y / $item->y); |
746
|
17295
|
|
|
|
|
28504
|
my $original_count_per_layer = $original_x_per_layer * $original_y_per_layer; |
747
|
17295
|
|
|
|
|
504194
|
my $new_count_per_layer = int($box->x / $item->y) * int($box->y / $item->x); |
748
|
17295
|
50
|
66
|
|
|
126309
|
if ( $new_count_per_layer > $original_count_per_layer # you can fit more items per layer in a swap |
|
|
|
66
|
|
|
|
|
749
|
|
|
|
|
|
|
|| $original_x_per_layer == 0 || $original_y_per_layer == 0 # if we keep it the current rotation we definitely won't fit, probably due to previous rotation |
750
|
|
|
|
|
|
|
) { |
751
|
5
|
|
|
|
|
32
|
$log->info('Rotating '.$item->{name}.', because we can fit more per layer if we rotate.'); |
752
|
5
|
|
|
|
|
252
|
my $temp_x = $item->x; |
753
|
5
|
|
|
|
|
150
|
$item->x($item->y); |
754
|
5
|
|
|
|
|
156
|
$item->y($temp_x); |
755
|
|
|
|
|
|
|
} |
756
|
|
|
|
|
|
|
} |
757
|
|
|
|
|
|
|
} |
758
|
|
|
|
|
|
|
else { |
759
|
0
|
|
|
|
|
0
|
$log->error('Item has a zero (0) dimension. That should not happen.'); |
760
|
|
|
|
|
|
|
} |
761
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
# pack the item into the box |
763
|
17295
|
100
|
|
|
|
53835
|
unless ($box->pack_item($item)) { |
764
|
21
|
100
|
|
|
|
72
|
if ($countdown) { # we still have other boxes to try |
765
|
9
|
|
|
|
|
35
|
$log->info("moving to next box type"); |
766
|
9
|
|
|
|
|
426
|
next BOXTYPE; |
767
|
|
|
|
|
|
|
} |
768
|
|
|
|
|
|
|
else { # no more boxes to try, time for spanning |
769
|
12
|
50
|
|
|
|
29
|
if (scalar(@{$self->boxes}) > $item_count) { |
|
12
|
|
|
|
|
376
|
|
770
|
0
|
|
|
|
|
0
|
$log->warn("More boxes than items."); |
771
|
|
|
|
|
|
|
#ouch 'more boxes than items', 'The number of boxes has exceded the number of items, which should never happen.'; |
772
|
|
|
|
|
|
|
} |
773
|
12
|
|
|
|
|
115
|
$log->info("no more box types, spanning"); |
774
|
12
|
|
|
|
|
600
|
$self->push_boxes($box); |
775
|
12
|
|
|
|
|
62
|
$box = $self->make_box($box_type); |
776
|
12
|
|
|
|
|
56
|
redo ITEM; |
777
|
|
|
|
|
|
|
} |
778
|
|
|
|
|
|
|
} |
779
|
|
|
|
|
|
|
} |
780
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
# we made it through our entire item list, yay! |
782
|
10
|
|
|
|
|
48
|
$log->info("finished!"); |
783
|
10
|
|
|
|
|
506
|
$self->push_boxes($box); |
784
|
10
|
|
|
|
|
285
|
last BOXTYPE; |
785
|
|
|
|
|
|
|
} |
786
|
|
|
|
|
|
|
} |
787
|
|
|
|
|
|
|
|
788
|
|
|
|
|
|
|
=head2 packing_list() |
789
|
|
|
|
|
|
|
|
790
|
|
|
|
|
|
|
Returns a data structure with all the item names and quantities packed into boxes. This can be used to generate manifests. |
791
|
|
|
|
|
|
|
|
792
|
|
|
|
|
|
|
[ |
793
|
|
|
|
|
|
|
{ # box one |
794
|
|
|
|
|
|
|
id => "xxx", |
795
|
|
|
|
|
|
|
name => "big box", |
796
|
|
|
|
|
|
|
weight => 30.1, |
797
|
|
|
|
|
|
|
packing_list => { |
798
|
|
|
|
|
|
|
"soda" => 3, |
799
|
|
|
|
|
|
|
"apple" => 1, |
800
|
|
|
|
|
|
|
"water bottle" => 2, |
801
|
|
|
|
|
|
|
} |
802
|
|
|
|
|
|
|
} |
803
|
|
|
|
|
|
|
] |
804
|
|
|
|
|
|
|
|
805
|
|
|
|
|
|
|
=cut |
806
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
sub packing_list { |
808
|
2
|
|
|
2
|
1
|
49142
|
my $self = shift; |
809
|
2
|
|
|
|
|
5
|
my @boxes; |
810
|
2
|
|
|
|
|
6
|
foreach my $box (@{$self->boxes}) { |
|
2
|
|
|
|
|
75
|
|
811
|
14
|
|
|
|
|
51
|
my ($weight, $list) = $box->packing_list; |
812
|
14
|
|
|
|
|
419
|
push @boxes, { |
813
|
|
|
|
|
|
|
id => $box->id, |
814
|
|
|
|
|
|
|
name => $box->name, |
815
|
|
|
|
|
|
|
weight => $weight, |
816
|
|
|
|
|
|
|
packing_list => $list, |
817
|
|
|
|
|
|
|
}; |
818
|
|
|
|
|
|
|
} |
819
|
2
|
|
|
|
|
13
|
return \@boxes; |
820
|
|
|
|
|
|
|
} |
821
|
|
|
|
|
|
|
|
822
|
|
|
|
|
|
|
=head2 packing_instructions() |
823
|
|
|
|
|
|
|
|
824
|
|
|
|
|
|
|
Returns a data structure with all the item names individually packed into rows, layers, and boxes. This can be used to build documentation on how to pack a set of boxes, and to generate a complete build history. |
825
|
|
|
|
|
|
|
|
826
|
|
|
|
|
|
|
[ |
827
|
|
|
|
|
|
|
{ # box one |
828
|
|
|
|
|
|
|
id => "xxx", |
829
|
|
|
|
|
|
|
name => "big box", |
830
|
|
|
|
|
|
|
layers => [ |
831
|
|
|
|
|
|
|
{ # layer one |
832
|
|
|
|
|
|
|
rows => [ |
833
|
|
|
|
|
|
|
{ # row one |
834
|
|
|
|
|
|
|
items => [ |
835
|
|
|
|
|
|
|
{ # item one |
836
|
|
|
|
|
|
|
name => "apple", |
837
|
|
|
|
|
|
|
... |
838
|
|
|
|
|
|
|
}, |
839
|
|
|
|
|
|
|
... |
840
|
|
|
|
|
|
|
], |
841
|
|
|
|
|
|
|
}, |
842
|
|
|
|
|
|
|
... |
843
|
|
|
|
|
|
|
], |
844
|
|
|
|
|
|
|
... |
845
|
|
|
|
|
|
|
}, |
846
|
|
|
|
|
|
|
], |
847
|
|
|
|
|
|
|
}, |
848
|
|
|
|
|
|
|
] |
849
|
|
|
|
|
|
|
|
850
|
|
|
|
|
|
|
=cut |
851
|
|
|
|
|
|
|
|
852
|
|
|
|
|
|
|
sub packing_instructions { |
853
|
4
|
|
|
4
|
1
|
1641
|
my $self = shift; |
854
|
4
|
|
|
|
|
8
|
my @boxes = map { $_->packing_instructions} @{ $self->boxes }; |
|
16
|
|
|
|
|
88
|
|
|
4
|
|
|
|
|
130
|
|
855
|
4
|
|
|
|
|
31
|
return \@boxes; |
856
|
|
|
|
|
|
|
} |
857
|
|
|
|
|
|
|
|
858
|
|
|
|
|
|
|
=head1 TODO |
859
|
|
|
|
|
|
|
|
860
|
|
|
|
|
|
|
There are some additional optimizations that could be done to speed things up a bit. We might also be able to get a better fill percentage (less void space), although that's not really the intent of Box::Calc. |
861
|
|
|
|
|
|
|
|
862
|
|
|
|
|
|
|
=head1 PREREQS |
863
|
|
|
|
|
|
|
|
864
|
|
|
|
|
|
|
L<Moose> |
865
|
|
|
|
|
|
|
L<Ouch> |
866
|
|
|
|
|
|
|
L<Log::Any> |
867
|
|
|
|
|
|
|
L<Data::GUID> |
868
|
|
|
|
|
|
|
|
869
|
|
|
|
|
|
|
=head1 SUPPORT |
870
|
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
=over |
872
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
=item Repository |
874
|
|
|
|
|
|
|
|
875
|
|
|
|
|
|
|
L<http://github.com/rizen/Box-Calc> |
876
|
|
|
|
|
|
|
|
877
|
|
|
|
|
|
|
=item Bug Reports |
878
|
|
|
|
|
|
|
|
879
|
|
|
|
|
|
|
L<http://github.com/rizen/Box-Calc/issues> |
880
|
|
|
|
|
|
|
|
881
|
|
|
|
|
|
|
=back |
882
|
|
|
|
|
|
|
|
883
|
|
|
|
|
|
|
|
884
|
|
|
|
|
|
|
=head1 SEE ALSO |
885
|
|
|
|
|
|
|
|
886
|
|
|
|
|
|
|
Although these modules don't solve the same problem as this module, they may help you build something that does if Box::Calc doesn't quite help you do what you want. |
887
|
|
|
|
|
|
|
|
888
|
|
|
|
|
|
|
=over |
889
|
|
|
|
|
|
|
|
890
|
|
|
|
|
|
|
=item L<Algorithm::Knapsack> |
891
|
|
|
|
|
|
|
|
892
|
|
|
|
|
|
|
=item L<Algorithm::Bucketizer> |
893
|
|
|
|
|
|
|
|
894
|
|
|
|
|
|
|
=item L<Algorithm::Knap01DP> |
895
|
|
|
|
|
|
|
|
896
|
|
|
|
|
|
|
=back |
897
|
|
|
|
|
|
|
|
898
|
|
|
|
|
|
|
=head1 AUTHOR |
899
|
|
|
|
|
|
|
|
900
|
|
|
|
|
|
|
=over |
901
|
|
|
|
|
|
|
|
902
|
|
|
|
|
|
|
=item JT Smith <jt_at_plainblack_dot_com> |
903
|
|
|
|
|
|
|
|
904
|
|
|
|
|
|
|
=item Colin Kuskie <colink_at_plainblack_dot_com> |
905
|
|
|
|
|
|
|
|
906
|
|
|
|
|
|
|
=back |
907
|
|
|
|
|
|
|
|
908
|
|
|
|
|
|
|
=head1 LEGAL |
909
|
|
|
|
|
|
|
|
910
|
|
|
|
|
|
|
Box::Calc is Copyright 2012 Plain Black Corporation (L<http://www.plainblack.com>) and is licensed under the same terms as Perl itself. |
911
|
|
|
|
|
|
|
|
912
|
|
|
|
|
|
|
=cut |
913
|
|
|
|
|
|
|
|
914
|
7
|
|
|
7
|
|
18828
|
no Moose; |
|
7
|
|
|
|
|
16
|
|
|
7
|
|
|
|
|
59
|
|
915
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |