seshbot-packing 0.8.1 → 0.8.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3134dc89051dbe2bb688dc5608eee3ecc73847d141561f9a78376a414130039d
4
- data.tar.gz: eb87d9d7d2f0a5ea8e3b972201101dd6eaeed1f207637e38a4c68a9e607c7f43
3
+ metadata.gz: c1693c794a4be401fe157bd5a6385e1375e37c738e8919a168cc881ec262f9ec
4
+ data.tar.gz: 44c25e149bc8dba14cc2cc49f309b5e3ba5901bf4a636f89787ec5e946e28502
5
5
  SHA512:
6
- metadata.gz: 75385b37a1a5b0304483316012a6b626120861523701087ca5ea19ebbf0adf7a236d66783e7385206009989b4578f836bf44ddd2226914fafc5bd5a2da250a8a
7
- data.tar.gz: 48d84bdb45a986d0cb166f2d073965f38c8aba4ebacf9456e8ed980a4e33b06ee60c33722c60c551c6863f316c4ac73663f0512e678769d166a2059ccd018970
6
+ metadata.gz: 59f4cb5bad8c401ef6dde61443937c3bbce68f37f659aef074175a067fd6ea91b346531bc1c809d7b2788a30969eb14c92d8f716fc382d56ac26560cec6c93fd
7
+ data.tar.gz: c9536fe10e717e4841e8521f5986e467cc0e2b029c3a7512cee255f61941f0d0a5041c6ff0a9a047c218691d9b47ef69b14da6df7227f304cc4eb015f0ba1afa
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- seshbot-packing (0.8.0)
4
+ seshbot-packing (0.8.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,3 +1,4 @@
1
1
  require "seshbot/packing/version"
2
2
  require "seshbot/packing/package"
3
3
  require "seshbot/packing/recipe"
4
+ require "seshbot/packing/line_item"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seshbot
4
+ module Packing
5
+ # :nodocs:
6
+ class LineItem
7
+ attr_accessor :sku, :quantity, :price
8
+
9
+ def initialize(sku, quantity, price=0)
10
+ raise 'SKU must be a string' if sku.class != String
11
+ raise 'Quantity must be an integer' if quantity.class != Integer
12
+
13
+ @sku = sku
14
+ @quantity = quantity
15
+ @price = price
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,35 +5,46 @@ module Seshbot
5
5
  module Packing
6
6
  class Error < StandardError; end
7
7
  class << self
8
+ @@logger = nil
8
9
  def logger
9
- Logger.new(STDERR)
10
+ if @@logger.nil?
11
+ @@logger = Logger.new(STDERR)
12
+ @@logger.level = 1
13
+ end
14
+ @@logger
10
15
  end
11
16
 
12
17
  def filter_by_sku(items, sku_fragment, inverse: false)
13
18
  items.select do |i|
14
- matches = i['sku'].include?(sku_fragment)
19
+ matches = i.sku.include?(sku_fragment)
15
20
  inverse ? !matches : matches
16
21
  end
17
22
  end
18
23
 
19
24
  def substitute_sku(items, from, to)
20
- items.each { |i| i['sku'].gsub!(from, to) }
21
- items
25
+ items.map { |i| Seshbot::Packing::LineItem.new(i.sku.gsub(from, to), i.quantity) }
22
26
  end
23
27
 
24
28
  def count_product_types(items)
25
29
  result = Hash.new(0)
26
30
  # create a hash were the keys are sku_fragment, and the values are the summed quantities
27
31
  items.each do |item|
28
- size = packaged_size(item["sku"])
29
- result[size] += item["quantity"]
32
+ size = packaged_size(item.sku)
33
+ result[size] += item.quantity
30
34
  end
31
- result
35
+ result.map { |k,v| Seshbot::Packing::LineItem.new("BUND-#{k}", v) }
32
36
  end
33
37
 
34
38
  def packaged_size(sku)
35
- # Takes an sku and returns the size
36
- result = sku[/[LUP]\d{2}|[BCM]\d{3}/]
39
+ size_regex = /-(K?[LUP]\d{2}|[BCM]\d{3})/
40
+ match = sku.match(size_regex)
41
+ raise "Cannot determine packaged size for SKU: #{sku}" if match.nil? || match.captures.empty?
42
+
43
+ # this is everything inside the (..) in the regex (everything except the leading '-')
44
+ result = match.captures[0]
45
+ # HACK! for legacy reasons, we don't consider K to be part of the size.
46
+ result.gsub!(/^K/, '')
47
+
37
48
  if result == "B502"
38
49
  result = "B306"
39
50
  end
@@ -45,20 +56,19 @@ module Seshbot
45
56
  result
46
57
  end
47
58
 
48
- def bundle_items(items, fulfilled_at: nil)
59
+ def bundle_items(recipes, items, fulfilled_at: nil)
49
60
  logger.debug "***** BUNDLING"
50
61
  logger.debug "Bundling Items: #{items}. Fulfilled at: #{fulfilled_at || '(no fulfillment date)'}"
51
62
  if !fulfilled_at.nil? && fulfilled_at < DateTime.new(2020, 1, 14, 8, 0, 0, Time.new.zone)
52
- return items.map { |item| { 'sku' => item.sku, 'quantity' => item.quantity, 'price' => item.price } }
63
+ return items
53
64
  end
54
65
 
55
66
  # get the cans first, and separate them out
56
67
  cans = filter_by_sku(items, '-C')
57
68
  unless cans.empty?
58
- product_type_counts = count_product_types(cans)
59
- product_type_counts = unpack(product_type_counts)
60
- product_type_counts = pack(product_type_counts)
61
- new_can_items = product_type_counts.map { |k, v| { 'sku' => "BUND-#{k}", 'quantity' => v, 'price' => 0 } }
69
+ bundle_items_by_type = count_product_types(cans)
70
+ bundle_items_by_type = unpack(recipes, bundle_items_by_type)
71
+ new_can_items = pack(recipes, bundle_items_by_type)
62
72
  # separate out all the C324s
63
73
  separated_c324s = filter_by_sku(new_can_items, '-C324')
64
74
  logger.debug "Cans: #{cans}"
@@ -76,16 +86,15 @@ module Seshbot
76
86
  logger.debug " - remaining items: #{remaining_items}"
77
87
 
78
88
  # substitute C's for B's
79
- substitute_sku(remaining_items, '-C', '-B')
89
+ remaining_items = substitute_sku(remaining_items, '-C', '-B')
80
90
  logger.debug " - skus updated to: #{remaining_items}"
81
91
 
82
92
  # get a hash of {pack_6: 5, pack_12: 1}
83
- product_type_counts = count_product_types(remaining_items)
93
+ bundle_items_by_type = count_product_types(remaining_items)
84
94
  # dismantle packages into individual units (e.g., {pack_6: 7})
85
- product_type_counts = unpack(product_type_counts)
95
+ bundle_items_by_type = unpack(recipes, bundle_items_by_type)
86
96
  # repackage into the 'best' packaging we can figure out (e.g., {pack_12: 2})
87
- product_type_counts = pack(product_type_counts)
88
- new_remaining_items = product_type_counts.map { |k,v| { "sku" => "BUND-#{k}", "quantity" => v, "price" => 0} }
97
+ new_remaining_items = pack(recipes, bundle_items_by_type)
89
98
 
90
99
  separated_c324s ||= []
91
100
  merged_items = new_remaining_items + separated_c324s
@@ -95,45 +104,47 @@ module Seshbot
95
104
  merged_items
96
105
  end
97
106
 
107
+ def unpack(recipes, items)
98
108
 
99
- def unpack(product_type_counts)
100
- final_result = product_type_counts
109
+ final_result = items
101
110
 
102
- while true
103
- new_result = pack_single_step(final_result, unpacking: true)
104
- break if new_result == final_result
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
105
114
 
106
115
  final_result = new_result
107
116
  end
108
117
  final_result
109
118
  end
110
119
 
111
- def pack_single_step(product_type_counts, unpacking:)
120
+ def pack_single_step(recipes, items, unpacking:)
112
121
  result = Hash.new(0)
113
- product_type_counts.each do |sku_fragment, qty|
114
- recipe = Recipe::find_best_recipe(sku_fragment, qty, unpacking: unpacking)
122
+ items.each do |item|
123
+ sku_size = packaged_size(item.sku)
124
+ recipe = Recipe::find_best_recipe(recipes, sku_size, item.quantity, unpacking: unpacking)
115
125
  if recipe.nil?
116
- result[sku_fragment] += qty
126
+ result[sku_size] += item.quantity
117
127
  next
118
128
  end
119
129
 
120
- recipe_quantities = Recipe::apply_recipe(recipe, qty)
130
+ recipe_quantities = Recipe::apply_recipe(recipe, item.quantity)
121
131
  result[recipe["input_fragment"]] += recipe_quantities[:input_quantity]
122
132
  result[recipe["output_fragment"]] += recipe_quantities[:output_quantity]
123
133
  end
124
134
  result.delete_if { |k,v| v == 0}
125
- result
135
+ result.map { |k,v| Seshbot::Packing::LineItem.new("BUND-#{k}", v) }
126
136
  end
127
137
 
128
138
 
129
- def pack(product_type_counts)
130
- final_result = product_type_counts
139
+ def pack(recipes, items)
140
+ final_result = items
131
141
 
132
142
  # keep trying to 'pack' until it stabilises (e.g., 6x6pack => 1x24pack+2x6pack => 1x24pack+1x12pack)
133
- while true
134
- new_result = pack_single_step(final_result, unpacking: false)
143
+ loop do
144
+ new_result = pack_single_step(recipes, final_result, unpacking: false)
135
145
  # no changes, break out
136
- break if new_result == final_result
146
+ break if new_result.map { |li| [li.sku, li.quantity] }.sort == final_result.map { |li| [li.sku, li.quantity] }.sort
147
+
137
148
  final_result = new_result
138
149
  end
139
150
  final_result
@@ -2,61 +2,8 @@ module Seshbot
2
2
  module Packing
3
3
  module Recipe
4
4
  class << self
5
- def get_recipes
6
- {
7
- "bottles_twenty_four_pack_4"=>{
8
- "input_fragment"=>"B306",
9
- "input_quantity"=>4,
10
- "output_fragment"=>"B324",
11
- "output_quantity"=>1
12
- },
13
- "bottles_twenty_four_pack_3"=>{
14
- "input_fragment"=>"B306",
15
- "input_quantity"=>3,
16
- "output_fragment"=>"B318",
17
- "output_quantity"=>1
18
- },
19
- "bottles_twelve_pack"=>{
20
- "input_fragment"=>"B306",
21
- "input_quantity"=>2,
22
- "output_fragment"=>"B312",
23
- "output_quantity"=>1
24
- },
25
- "bottles_six_pack"=>{
26
- "input_fragment"=>"B301",
27
- "input_quantity"=>6,
28
- "output_fragment"=>"B306",
29
- "output_quantity"=>1
30
- },
31
- "cans_twenty_four_pack_4"=>{
32
- "input_fragment"=>"C306",
33
- "input_quantity"=>4,
34
- "output_fragment"=>"C324",
35
- "output_quantity"=>1
36
- },
37
- "cans_twenty_four_pack_3"=>{
38
- "input_fragment"=>"C306",
39
- "input_quantity"=>3,
40
- "output_fragment"=>"C318",
41
- "output_quantity"=>1
42
- },
43
- "cans_twelve_pack"=>{
44
- "input_fragment"=>"C306",
45
- "input_quantity"=>2,
46
- "output_fragment"=>"C312",
47
- "output_quantity"=>1
48
- },
49
- "cans_six_pack"=>{
50
- "input_fragment"=>"C301",
51
- "input_quantity"=>6,
52
- "output_fragment"=>"C306",
53
- "output_quantity"=>1
54
- }
55
- }
56
- end
57
-
58
- def find_best_recipe(sku_fragment, qty, unpacking: false)
59
- recipes = find_recipes(sku_fragment, qty, unpacking: unpacking)
5
+ def find_best_recipe(recipes_hash, sku_fragment, qty, unpacking: false)
6
+ recipes = find_recipes(recipes_hash, sku_fragment, qty, unpacking: unpacking)
60
7
  # we want the recipe that is 'best' (packaging into as few items as possible, or unpacaging into as many as possible)
61
8
  best_recipe_quantities = nil
62
9
  recipes.each do |name, recipe|
@@ -77,8 +24,7 @@ module Seshbot
77
24
  best_recipe_quantities[:recipe] if best_recipe_quantities
78
25
  end
79
26
 
80
- def find_recipes(sku_fragment, qty, unpacking: false)
81
- recipes = get_recipes
27
+ def find_recipes(recipes, sku_fragment, qty, unpacking: false)
82
28
  recipes = reverse_recipes(recipes) if unpacking
83
29
  recipes.select do |name, recipe|
84
30
  recipe["input_fragment"] == sku_fragment && qty >= recipe["input_quantity"]
@@ -1,5 +1,5 @@
1
1
  module Seshbot
2
2
  module Packing
3
- VERSION = '0.8.1'
3
+ VERSION = '0.8.6'
4
4
  end
5
5
  end
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.8.1
4
+ version: 0.8.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaun
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-13 00:00:00.000000000 Z
11
+ date: 2021-03-31 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -28,6 +28,7 @@ files:
28
28
  - bin/console
29
29
  - bin/setup
30
30
  - lib/seshbot/packing.rb
31
+ - lib/seshbot/packing/line_item.rb
31
32
  - lib/seshbot/packing/package.rb
32
33
  - lib/seshbot/packing/recipe.rb
33
34
  - lib/seshbot/packing/version.rb