seshbot-packing 0.8.6 → 0.9.1
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +3 -1
- data/README.md +43 -3
- data/lib/seshbot/packing/line_item.rb +28 -6
- data/lib/seshbot/packing/package.rb +109 -94
- data/lib/seshbot/packing/recipe.rb +104 -36
- data/lib/seshbot/packing/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a3ee88fef7cdb7044eb06951aaee1a388dc499a33226d553ecf1d4af3865df2
|
4
|
+
data.tar.gz: 7ceb6a0a5ad57b515e881352907e42c3bec29330c1055a8947e9662f03ad0ebc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7d04a459fbfc6dc06b7b028b535f5a1ab8e87579eb59f3aebc6803d82587abd7582c94074217e6935e189ba56d185138841aca429c3e774ff872c6e82e4a93c
|
7
|
+
data.tar.gz: 030363e4fe38d1f095766f836b86960639e8f3a23cbdde611c3d305cb259315816adc0a488cde7134f778958d3022f19056a0f850244324e0a844bc855195ba9
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
seshbot-packing (0.
|
4
|
+
seshbot-packing (0.9.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
minitest (5.14.2)
|
10
10
|
rake (12.3.3)
|
11
|
+
timecop (0.9.4)
|
11
12
|
|
12
13
|
PLATFORMS
|
13
14
|
ruby
|
@@ -16,6 +17,7 @@ DEPENDENCIES
|
|
16
17
|
minitest (~> 5.0)
|
17
18
|
rake (~> 12.0)
|
18
19
|
seshbot-packing!
|
20
|
+
timecop
|
19
21
|
|
20
22
|
BUNDLED WITH
|
21
23
|
2.1.4
|
data/README.md
CHANGED
@@ -1,5 +1,45 @@
|
|
1
1
|
# Seshbot::Packing
|
2
2
|
|
3
|
+
This gem allows inventory to be packed for shipping - used by BrewKeeper for pick/pack logic, and by Carrierbot to aid in calculating 'bundled' shipping rates
|
4
|
+
|
5
|
+
## Recipes
|
6
|
+
|
7
|
+
The packing rules are described as 'recipes' - one of which is represented as a hash of the format:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
{
|
11
|
+
'input_fragment': 'B306',
|
12
|
+
'input_quantity': 1,
|
13
|
+
'output_fragment': 'B301',
|
14
|
+
'output_quantity': '6',
|
15
|
+
'is_reversible': true # optional
|
16
|
+
}
|
17
|
+
```
|
18
|
+
|
19
|
+
The process of packing a collection of line-items in a customer shipment into a smplified set of line-items representing the most cost effective way to pack those items is as follows:
|
20
|
+
|
21
|
+
* get all quantity+SKUs in the order/fulfillments in the request
|
22
|
+
* unpack as much as possible by:
|
23
|
+
** reverse all recipes inputs/outputs
|
24
|
+
** find the ‘best’ recipe (the recipe that gives the MOST outputs)
|
25
|
+
** repeat until no more unpacking can be done
|
26
|
+
* pack as much as possible by:
|
27
|
+
** find the ‘best’ recipe (the recipe that gives the FEWEST outputs)
|
28
|
+
** repeat until no more packing can be done
|
29
|
+
|
30
|
+
Therre area few special considerations:
|
31
|
+
- optional `is_reversible` flag: some recipes may not deemed reversible for the 'unpacking' phase; e.g., 3xB306->1xB324. These should be marked as 'is_reversible': false
|
32
|
+
- optional `phase` flag: packing recipes may be performed in 'phases' in order to ensure certain rules are adhered to for specific products or variant types (e.g., pack cans into special can-cartons before allowing the remainders to be mixed in with bottles)
|
33
|
+
|
34
|
+
Note that if a recipe is found, _it will be applied_ unless a better rule is found.
|
35
|
+
|
36
|
+
## Building
|
37
|
+
|
38
|
+
* Update the VERSION file
|
39
|
+
* `gem build seshbot-packing.gemspec`
|
40
|
+
* commit changes
|
41
|
+
* `gem push seshbot-packing-VERSION.gem`
|
42
|
+
|
3
43
|
## Installation
|
4
44
|
|
5
45
|
Add this line to your application's Gemfile:
|
@@ -35,15 +75,15 @@ Seshbot::Packing.count_product_types(items)
|
|
35
75
|
## returns the hashes where the sku value matches the given string
|
36
76
|
items = [{ 'sku' => 'A-C306', 'quantity' => 4, 'price' => 1 },
|
37
77
|
{ 'sku' => 'A-B306', 'quantity' => 4, 'price' => 1 }]
|
38
|
-
Seshbot::Packing.
|
78
|
+
Seshbot::Packing._filter_by_sku(items, '-C')
|
39
79
|
#=> [{ 'sku' => 'A-C306', 'quantity' => 4, 'price' => 1 }]
|
40
80
|
## also takes a named argument
|
41
|
-
Seshbot::Packing.
|
81
|
+
Seshbot::Packing._filter_by_sku(items, '-C', inverse: true)
|
42
82
|
#=> [{ 'sku' => 'A-B306', 'quantity' => 4, 'price' => 1 }]
|
43
83
|
|
44
84
|
# Substitute SKU
|
45
85
|
## replaces an sku in a hash
|
46
|
-
Seshbot::Packing.
|
86
|
+
Seshbot::Packing._substitute_sku(items, '-B', '-C')
|
47
87
|
#=> items = [{"sku" => "A-C306", "quantity" => 2}, {"sku" => "B-C306", "quantity" => 1}]
|
48
88
|
```
|
49
89
|
|
@@ -4,16 +4,38 @@ module Seshbot
|
|
4
4
|
module Packing
|
5
5
|
# :nodocs:
|
6
6
|
class LineItem
|
7
|
-
attr_accessor :
|
7
|
+
attr_accessor :sku_fragment, :quantity, :price
|
8
8
|
|
9
|
-
def initialize(
|
10
|
-
raise 'SKU must be a string' if
|
9
|
+
def initialize(sku_fragment, quantity, price=0)
|
10
|
+
raise 'SKU fragment must be a string' if sku_fragment.class != String
|
11
11
|
raise 'Quantity must be an integer' if quantity.class != Integer
|
12
12
|
|
13
|
-
@
|
13
|
+
@sku_fragment = sku_fragment
|
14
14
|
@quantity = quantity
|
15
15
|
@price = price
|
16
16
|
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{quantity}x#{sku_fragment}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.summarise(items)
|
23
|
+
"#{items.map(&:to_s)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.total_quantity(items)
|
27
|
+
items.map { |i| i.quantity }.sum
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.merge_line_items(items)
|
31
|
+
result = Hash.new(0)
|
32
|
+
# create a hash were the keys are sku_fragment, and the values are the summed quantities
|
33
|
+
items.each do |item|
|
34
|
+
result[item.sku_fragment] += item.quantity
|
35
|
+
end
|
36
|
+
result.delete_if { |k,v| v == 0 }
|
37
|
+
result.map { |sku_fragment, quantity| LineItem.new(sku_fragment, quantity) }
|
38
|
+
end
|
17
39
|
end
|
18
|
-
end
|
19
|
-
end
|
40
|
+
end # module Packing
|
41
|
+
end # module Seshbot
|
@@ -14,141 +14,156 @@ module Seshbot
|
|
14
14
|
@@logger
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
def bundle_items(recipes, items, fulfilled_at: nil)
|
18
|
+
if !fulfilled_at.nil? && fulfilled_at < DateTime.parse('2020-01-14T08:00:00+09:00')
|
19
|
+
logger.debug "== Not Bundling (pre-2020 order) =="
|
20
|
+
return items
|
21
21
|
end
|
22
|
-
|
22
|
+
is_legacy = _is_legacy(fulfilled_at)
|
23
|
+
is_legacy_str = is_legacy ? " (LEGACY)" : ""
|
23
24
|
|
24
|
-
|
25
|
-
items.map { |i| Seshbot::Packing::LineItem.new(i.sku.gsub(from, to), i.quantity) }
|
26
|
-
end
|
25
|
+
logger.debug "== Bundling Items: #{items.map(&:to_s)}. Fulfilled at: #{fulfilled_at || '(no fulfillment date)'}#{is_legacy_str}"
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
27
|
+
results = []
|
28
|
+
|
29
|
+
if is_legacy
|
30
|
+
results = _bundle_items_legacy(recipes, items)
|
31
|
+
else
|
32
|
+
phases = recipes.values.map { |r| r['phase'] || 0 }.map(&:to_i).uniq.sort
|
33
|
+
|
34
|
+
results = items
|
35
|
+
phases.each do |phase|
|
36
|
+
phase_recipes = recipes.select { |name, recipe| (recipe['phase'] || 0) == phase }.to_h
|
37
|
+
|
38
|
+
logger.debug "=== Bundling phase #{phase}"
|
39
|
+
|
40
|
+
results = _bundle_items_single_phase(phase_recipes, results)
|
41
|
+
end
|
34
42
|
end
|
35
|
-
|
43
|
+
|
44
|
+
logger.debug "Bundled result:"
|
45
|
+
logger.debug " - IN: #{items.map(&:to_s)}"
|
46
|
+
logger.debug " - OUT: #{results.map(&:to_s)}"
|
47
|
+
|
48
|
+
results
|
36
49
|
end
|
37
50
|
|
38
|
-
def
|
39
|
-
|
40
|
-
match = sku.match(size_regex)
|
41
|
-
raise "Cannot determine packaged size for SKU: #{sku}" if match.nil? || match.captures.empty?
|
51
|
+
def unpack(recipes, items)
|
52
|
+
prev_result = items
|
42
53
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
54
|
+
(1..1000).each do |i|
|
55
|
+
new_result = _pack_single_step(recipes, prev_result, unpacking: true)
|
56
|
+
is_unchanged = new_result.map { |li| [li.sku_fragment, li.quantity] }.sort == prev_result.map { |li| [li.sku_fragment, li.quantity] }.sort
|
57
|
+
return new_result if is_unchanged
|
47
58
|
|
48
|
-
|
49
|
-
result = "B306"
|
59
|
+
prev_result = new_result
|
50
60
|
end
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
raise "Cannot determine packaged size for SKU: #{sku}" if result.nil?
|
55
|
-
|
56
|
-
result
|
61
|
+
error_message = "could not unpack - infinite loop? (latest: #{prev_result.map(&:to_s)})"
|
62
|
+
logger.error error_message
|
63
|
+
raise error_message
|
57
64
|
end
|
58
65
|
|
59
|
-
def
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
66
|
+
def pack(recipes, items)
|
67
|
+
prev_result = items
|
68
|
+
|
69
|
+
# keep trying to 'pack' until it stabilises (e.g., 6x6pack => 1x24pack+2x6pack => 1x24pack+1x12pack)
|
70
|
+
(1..1000).each do |i|
|
71
|
+
new_result = _pack_single_step(recipes, prev_result, unpacking: false)
|
72
|
+
# no changes, break out
|
73
|
+
is_unchanged = new_result.map { |li| [li.sku_fragment, li.quantity] }.sort == prev_result.map { |li| [li.sku_fragment, li.quantity] }.sort
|
74
|
+
return new_result if is_unchanged
|
75
|
+
|
76
|
+
prev_result = new_result
|
64
77
|
end
|
78
|
+
error_message = "could not pack - infinite loop? (latest: #{prev_result.map(&:to_s)})"
|
79
|
+
logger.error error_message
|
80
|
+
raise error_message
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# private
|
85
|
+
#
|
86
|
+
|
87
|
+
def _is_legacy(fulfilled_at)
|
88
|
+
effective_fulfilled_at = fulfilled_at.nil? ? DateTime.now : fulfilled_at
|
89
|
+
is_legacy = effective_fulfilled_at < DateTime.parse('2021-04-07T09:00:00+09:00')
|
90
|
+
is_legacy
|
91
|
+
end
|
65
92
|
|
93
|
+
def _bundle_items_legacy(recipes, items)
|
66
94
|
# get the cans first, and separate them out
|
67
|
-
cans =
|
95
|
+
cans = _filter_by_sku_fragment_prefix(items, 'C')
|
96
|
+
remaining_cans = []
|
68
97
|
unless cans.empty?
|
69
|
-
bundle_items_by_type =
|
98
|
+
bundle_items_by_type = LineItem.merge_line_items(cans)
|
70
99
|
bundle_items_by_type = unpack(recipes, bundle_items_by_type)
|
71
100
|
new_can_items = pack(recipes, bundle_items_by_type)
|
72
101
|
# separate out all the C324s
|
73
|
-
separated_c324s =
|
74
|
-
logger.debug "Cans: #{cans}"
|
75
|
-
logger.debug " - Removed C324s: #{separated_c324s}"
|
102
|
+
separated_c324s = _filter_by_sku_fragment_prefix(new_can_items, 'C324')
|
103
|
+
logger.debug "Cans: #{cans.map(&:to_s)}"
|
104
|
+
logger.debug " - Removed C324s: #{separated_c324s.map(&:to_s)}"
|
76
105
|
|
77
|
-
remaining_cans =
|
78
|
-
logger.debug " - Remaining skus: #{remaining_cans}"
|
106
|
+
remaining_cans = _filter_by_sku_fragment_prefix(new_can_items, 'C324', inverse: true)
|
107
|
+
logger.debug " - Remaining skus: #{remaining_cans.map(&:to_s)}"
|
79
108
|
end
|
80
109
|
|
81
110
|
# merge the remaining with the original leftover
|
82
|
-
non_cans =
|
83
|
-
remaining_cans ||= []
|
111
|
+
non_cans = _filter_by_sku_fragment_prefix(items, 'C', inverse: true)
|
84
112
|
remaining_items = non_cans + remaining_cans
|
85
113
|
logger.debug "Substituting:"
|
86
|
-
logger.debug " - remaining items: #{remaining_items}"
|
114
|
+
logger.debug " - remaining items: #{remaining_items.map(&:to_s)}"
|
87
115
|
|
88
116
|
# substitute C's for B's
|
89
|
-
remaining_items =
|
90
|
-
logger.debug " - skus updated to: #{remaining_items}"
|
117
|
+
remaining_items = _substitute_sku(remaining_items, /^C/, 'B')
|
118
|
+
logger.debug " - skus updated to: #{remaining_items.map(&:to_s)}"
|
91
119
|
|
92
120
|
# get a hash of {pack_6: 5, pack_12: 1}
|
93
|
-
bundle_items_by_type =
|
121
|
+
bundle_items_by_type = LineItem.merge_line_items(remaining_items)
|
94
122
|
# dismantle packages into individual units (e.g., {pack_6: 7})
|
95
123
|
bundle_items_by_type = unpack(recipes, bundle_items_by_type)
|
96
124
|
# repackage into the 'best' packaging we can figure out (e.g., {pack_12: 2})
|
97
125
|
new_remaining_items = pack(recipes, bundle_items_by_type)
|
98
126
|
|
99
127
|
separated_c324s ||= []
|
100
|
-
|
101
|
-
logger.debug "Bundled result:"
|
102
|
-
logger.debug " - IN: #{items}"
|
103
|
-
logger.debug " - OUT: #{merged_items}"
|
104
|
-
merged_items
|
105
|
-
end
|
128
|
+
results = new_remaining_items + separated_c324s
|
106
129
|
|
107
|
-
|
108
|
-
|
109
|
-
final_result = items
|
110
|
-
|
111
|
-
loop do
|
112
|
-
new_result = pack_single_step(recipes, final_result, unpacking: true)
|
113
|
-
break if new_result.map { |li| [li.sku, li.quantity] }.sort == final_result.map { |li| [li.sku, li.quantity] }.sort
|
114
|
-
|
115
|
-
final_result = new_result
|
116
|
-
end
|
117
|
-
final_result
|
130
|
+
results
|
118
131
|
end
|
119
132
|
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
end
|
133
|
+
def _bundle_items_single_phase(recipes, items)
|
134
|
+
# get a list of items merged together by variant type (e.g., 10xBIGI-C301 + 5xBISS-C301 => 15xBUND-C301))
|
135
|
+
merged_items = LineItem.merge_line_items(items)
|
136
|
+
# dismantle packages into individual units
|
137
|
+
unpacked_items = unpack(recipes, merged_items)
|
138
|
+
# repackage into the 'best' packaging we can figure out
|
139
|
+
pack(recipes, unpacked_items)
|
140
|
+
end
|
129
141
|
|
130
|
-
|
131
|
-
|
132
|
-
|
142
|
+
def _filter_by_sku_fragment_prefix(items, sku_fragment, inverse: false)
|
143
|
+
items.select do |i|
|
144
|
+
matches = i.sku_fragment.start_with? sku_fragment
|
145
|
+
inverse ? !matches : matches
|
133
146
|
end
|
134
|
-
result.delete_if { |k,v| v == 0}
|
135
|
-
result.map { |k,v| Seshbot::Packing::LineItem.new("BUND-#{k}", v) }
|
136
147
|
end
|
137
148
|
|
149
|
+
def _substitute_sku(items, re, sub)
|
150
|
+
items.map { |i| LineItem.new(i.sku_fragment.gsub(re, sub), i.quantity) }
|
151
|
+
end
|
138
152
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
new_result = pack_single_step(recipes, final_result, unpacking: false)
|
145
|
-
# no changes, break out
|
146
|
-
break if new_result.map { |li| [li.sku, li.quantity] }.sort == final_result.map { |li| [li.sku, li.quantity] }.sort
|
147
|
-
|
148
|
-
final_result = new_result
|
153
|
+
def _pack_single_step(recipes, items, unpacking:)
|
154
|
+
recipe = Recipe::find_best_recipe(recipes, items, unpacking: unpacking)
|
155
|
+
if recipe.nil?
|
156
|
+
# its as packed/unpacked as it can get
|
157
|
+
return items
|
149
158
|
end
|
150
|
-
|
159
|
+
|
160
|
+
before = LineItem::summarise(items)
|
161
|
+
items = Recipe::apply_recipe(recipe, items)
|
162
|
+
after = LineItem::summarise(items)
|
163
|
+
logger.debug "applying recipe (#{unpacking ? 'unpacking' : 'packing'}) #{Recipe::summarise(recipe)}: #{before} -> #{after}"
|
164
|
+
items
|
151
165
|
end
|
152
|
-
|
153
|
-
|
154
|
-
end
|
166
|
+
|
167
|
+
end # class << self
|
168
|
+
end # module Packing
|
169
|
+
end # module Seshbot
|
@@ -2,59 +2,127 @@ module Seshbot
|
|
2
2
|
module Packing
|
3
3
|
module Recipe
|
4
4
|
class << self
|
5
|
-
def find_best_recipe(recipes_hash,
|
6
|
-
recipes =
|
7
|
-
# we want the recipe that is 'best' (packaging into as few items as possible, or unpacaging into as many as possible)
|
8
|
-
best_recipe_quantities = nil
|
9
|
-
recipes.each do |name, recipe|
|
10
|
-
recipe_quantities = apply_recipe(recipe, qty)
|
5
|
+
def find_best_recipe(recipes_hash, items, unpacking: false)
|
6
|
+
recipes = _find_recipes(recipes_hash, items, unpacking: unpacking)
|
11
7
|
|
12
|
-
|
13
|
-
|
8
|
+
# we want the recipe that is 'best' (packaging into as few items as possible, or unpacaging into as many as possible)
|
9
|
+
best_recipe = nil
|
10
|
+
best_recipe_output_quantity = nil
|
11
|
+
recipes.each do |recipe_name, recipe|
|
12
|
+
recipe_factor = _calculate_recipe_factor(recipe, items)
|
13
|
+
recipe_output_quantity = recipe_factor * recipe['output_quantity']
|
14
14
|
|
15
15
|
# is this recipe the 'best'? (if packaging, best is the one that creates fewest quantity)
|
16
|
-
is_best =
|
17
|
-
|
18
|
-
|
16
|
+
is_best = if best_recipe_output_quantity.nil?
|
17
|
+
true
|
18
|
+
else
|
19
|
+
unpacking ?
|
20
|
+
recipe_output_quantity > best_recipe_output_quantity :
|
21
|
+
recipe_output_quantity < best_recipe_output_quantity
|
22
|
+
end
|
19
23
|
|
20
24
|
if is_best
|
21
|
-
|
25
|
+
best_recipe_output_quantity = recipe_output_quantity
|
26
|
+
best_recipe = recipe
|
22
27
|
end
|
23
28
|
end
|
24
|
-
|
29
|
+
best_recipe
|
25
30
|
end
|
26
31
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
def apply_recipe(recipe, items)
|
33
|
+
factor = _calculate_recipe_factor(recipe, items)
|
34
|
+
|
35
|
+
# shortcut - cannot apply recipe, not enough inputs
|
36
|
+
return items if factor == 0
|
37
|
+
|
38
|
+
results = items.dup
|
39
|
+
|
40
|
+
# first remove inputs (add negative quantities)
|
41
|
+
recipe_inputs = recipe['inputs'].nil? ? [recipe] : recipe['inputs']
|
42
|
+
recipe_inputs.each do |inputs|
|
43
|
+
results << LineItem.new(inputs['input_fragment'], -1 * inputs['input_quantity'] * factor)
|
31
44
|
end
|
32
|
-
|
45
|
+
|
46
|
+
# now add input
|
47
|
+
results << LineItem.new(recipe['output_fragment'], recipe['output_quantity'] * factor)
|
48
|
+
|
49
|
+
# consolidate
|
50
|
+
results = LineItem.merge_line_items(results)
|
33
51
|
end
|
34
52
|
|
35
|
-
def
|
36
|
-
|
53
|
+
def summarise(recipe)
|
54
|
+
recipe_inputs = recipe['inputs'].nil? ? [recipe] : recipe['inputs']
|
55
|
+
inputs = recipe_inputs.map { |i| "#{i['input_quantity']}x#{i['input_fragment']}" }.join(',')
|
56
|
+
outputs = "#{recipe['output_quantity']}x#{recipe['output_fragment']}"
|
57
|
+
"#{inputs} -> #{outputs}"
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# private
|
62
|
+
#
|
63
|
+
|
64
|
+
def _calculate_recipe_factor(recipe, items)
|
65
|
+
recipe_inputs = recipe['inputs'].nil? ? [recipe] : recipe['inputs']
|
66
|
+
|
67
|
+
recipe_input_factors = recipe_inputs.map do |inputs|
|
68
|
+
input_fragment = inputs['input_fragment']
|
69
|
+
input_quantity = inputs['input_quantity']
|
70
|
+
|
71
|
+
item = items.find { |i| i.sku_fragment == input_fragment }
|
72
|
+
raise "recipe contains items not present in order (#{input_fragment})" if item.nil?
|
73
|
+
|
74
|
+
[inputs, item.quantity / input_quantity]
|
75
|
+
end
|
76
|
+
|
77
|
+
recipe_input_factors.map { |i,f| f }.min
|
78
|
+
end
|
79
|
+
|
80
|
+
def _find_recipes(recipes, items, unpacking: false)
|
81
|
+
recipes = _reverse_recipes(recipes) if unpacking
|
82
|
+
recipes.select do |recipe_name, recipe_details|
|
83
|
+
recipe_inputs = recipe_details['inputs'].nil? ? [recipe_details] : recipe_details['inputs']
|
84
|
+
|
85
|
+
# recipe is a candidate if all inputs may be satisfied by available items
|
86
|
+
recipe_inputs.all? do |inputs|
|
87
|
+
items.any? { |i| inputs["input_fragment"] == i.sku_fragment && i.quantity >= inputs["input_quantity"] }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def _reverse_recipes(recipes)
|
93
|
+
# first exclude recipes that are explicitly configured as not reversible
|
94
|
+
reversible_recipes = recipes.select do |recipe_name, recipe_details|
|
95
|
+
recipe_details['is_reversible'].nil? || recipe_details['is_reversible']
|
96
|
+
end
|
97
|
+
|
98
|
+
unique_reversible_output_fragments = reversible_recipes.values.map { |r| r['output_fragment'] }.sort.uniq
|
99
|
+
|
100
|
+
# further refine by excluding recipes that are technically incapable of being reversed
|
101
|
+
reversible_recipes = reversible_recipes.select do |recipe_name, recipe_details|
|
102
|
+
# currently dont support multipe outputs, so cannot reverse recipes with multiple inputs
|
103
|
+
is_composite = !recipe_details['inputs'].nil? && recipe_details['inputs'].length != 1
|
104
|
+
# cannot reverse recipes where there are multipe ways of creating the same output
|
105
|
+
is_ambiguous = !unique_reversible_output_fragments.include?(recipe_details['output_fragment'])
|
106
|
+
|
107
|
+
!is_composite && !is_ambiguous
|
108
|
+
end
|
109
|
+
|
110
|
+
results = reversible_recipes.map do |recipe_name, recipe_details|
|
111
|
+
# here we know that if 'inputs' is specified it will have exactly 1 input
|
112
|
+
input_fragment = recipe_details["input_fragment"] || recipe_details["inputs"][0]["input_fragment"]
|
113
|
+
input_quantity = recipe_details["input_quantity"] || recipe_details["inputs"][0]["input_quantity"]
|
114
|
+
|
37
115
|
new_recipe_details = {
|
38
116
|
"input_fragment" => recipe_details["output_fragment"],
|
39
117
|
"input_quantity" => recipe_details["output_quantity"],
|
40
|
-
"output_fragment" =>
|
41
|
-
"output_quantity" =>
|
118
|
+
"output_fragment" => input_fragment,
|
119
|
+
"output_quantity" => input_quantity
|
42
120
|
}
|
43
121
|
[recipe_name, new_recipe_details]
|
44
122
|
end
|
45
123
|
results.to_h
|
46
124
|
end
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
recipe: recipe,
|
52
|
-
input_quantity: remainder,
|
53
|
-
output_quantity: multiplier * recipe["output_quantity"]
|
54
|
-
}
|
55
|
-
end
|
56
|
-
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
125
|
+
end # class << self
|
126
|
+
end # module Recipe
|
127
|
+
end # module Packing
|
128
|
+
end # module Seshbot
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: seshbot-packing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shaun
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-04-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|