seshbot-packing 0.7.0 → 0.8.5

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: bc2128e8b492960b7f2884c5cf7867673d5c8aa57f1af7b9bcd33db76573fc9b
4
- data.tar.gz: 1e73660ee059d28c64c278d0e10cc34f3beee5ea693c74f74f795dec64cc95ea
3
+ metadata.gz: 1df1737c878348a458089c20c950a5a2c0a8fdd501736904f8a4c89cc83e92ad
4
+ data.tar.gz: bb96a3429acfbda5e066652600df576f6cf3a0a4e303ee3c6a68a1841eb984e8
5
5
  SHA512:
6
- metadata.gz: 274c0bd6812eca6f29cef6309a4cb9db65e20cb45b5e468dcc4f38815043980b7ceedb8fc415c2b8ee8c117f43fa7f476e9ff4ef7cc9ea9f840c84c358e35d3c
7
- data.tar.gz: b5596d94568c96088425011d4ebe48993b0b065f4d9d3dea3bc7bdad83f2d91227a8e4544062b76ba781b199875ab6acb1168d277fbd5f444ec6f40783ca2618
6
+ metadata.gz: 8557f78b9e1092f5ea8d776399d2ebc9b1163cd184a45e28482abf2635788b57e27f7dc6fd7d32e7b6b013eb62f86dddb71f3ff64c77f74e71c36278a24e6403
7
+ data.tar.gz: 52d32265eedae66704b42a43a6183a47bf4268007b463a6cc2529cc291b09c10ef5353b15b725add93931d7f273053fbfc46700a70e43091218b8106017f9e4d
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  .byebug_history
2
+ .ruby-version
2
3
  /.bundle/
3
4
  /.yardoc
4
5
  /_yardoc/
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## Version 0.8.0
2
+
3
+ * -C324 are calculated and separated from bundling
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- seshbot-packing (0.5.0)
4
+ seshbot-packing (0.8.5)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,9 +1,5 @@
1
1
  # Seshbot::Packing
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/seshbot/packing`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
6
-
7
3
  ## Installation
8
4
 
9
5
  Add this line to your application's Gemfile:
@@ -20,9 +16,43 @@ Or install it yourself as:
20
16
 
21
17
  $ gem install seshbot-packing
22
18
 
23
- ## Usage
19
+ ## Methods
20
+
21
+ ```ruby
22
+ # Bundle Items
23
+ ## this is the main method, it calls most other methods to get its result
24
+ items = [{ 'sku' => 'B-B306', 'quantity' => 2, 'price' => 1 }]
25
+ Seshbot::Packing.bundle_items(items)
26
+ #=> [{ 'sku' => 'BUND-B312', 'quantity' => 1, 'price' => 0 }]
27
+
28
+ # Count Product Types
29
+ ## groups together everythign after the `-` and counts them
30
+ items = [{"sku" => "A-B306", "quantity" => 2}, {"sku" => "B-B306", "quantity" => 1}]
31
+ Seshbot::Packing.count_product_types(items)
32
+ #=> {"B306"=>3}
33
+
34
+ # Separate SKU
35
+ ## returns the hashes where the sku value matches the given string
36
+ items = [{ 'sku' => 'A-C306', 'quantity' => 4, 'price' => 1 },
37
+ { 'sku' => 'A-B306', 'quantity' => 4, 'price' => 1 }]
38
+ Seshbot::Packing.filter_by_sku(items, '-C')
39
+ #=> [{ 'sku' => 'A-C306', 'quantity' => 4, 'price' => 1 }]
40
+ ## also takes a named argument
41
+ Seshbot::Packing.filter_by_sku(items, '-C', inverse: true)
42
+ #=> [{ 'sku' => 'A-B306', 'quantity' => 4, 'price' => 1 }]
43
+
44
+ # Substitute SKU
45
+ ## replaces an sku in a hash
46
+ Seshbot::Packing.substitute_sku(items, '-B', '-C')
47
+ #=> items = [{"sku" => "A-C306", "quantity" => 2}, {"sku" => "B-C306", "quantity" => 1}]
48
+ ```
49
+
24
50
 
25
- TODO: Write usage instructions here
51
+ ## C324s
52
+ As of v0.8.0, C324s will be treated differently. If there are any C324s that can exist, they will stay as C324s, and not converted to bottles.
53
+ eg.
54
+ In: 1x C312, 1x C312, 1x C306, 1x B306
55
+ Out: 1x C324, 1x B312
26
56
 
27
57
  ## Development
28
58
 
@@ -1,13 +1,32 @@
1
- require "date"
1
+ require 'logger'
2
+ require 'date'
3
+
2
4
  module Seshbot
3
5
  module Packing
4
6
  class Error < StandardError; end
5
-
6
7
  class << self
8
+ def logger
9
+ logger = Logger.new(STDERR)
10
+ logger.level = 1
11
+ logger
12
+ end
13
+
14
+ def filter_by_sku(items, sku_fragment, inverse: false)
15
+ items.select do |i|
16
+ matches = i['sku'].include?(sku_fragment)
17
+ inverse ? !matches : matches
18
+ end
19
+ end
20
+
21
+ def substitute_sku(items, from, to)
22
+ items.each { |i| i['sku'].gsub!(from, to) }
23
+ items
24
+ end
25
+
7
26
  def count_product_types(items)
8
27
  result = Hash.new(0)
9
28
  # create a hash were the keys are sku_fragment, and the values are the summed quantities
10
- items.each do |item|
29
+ items.each do |item|
11
30
  size = packaged_size(item["sku"])
12
31
  result[size] += item["quantity"]
13
32
  end
@@ -15,8 +34,15 @@ module Seshbot
15
34
  end
16
35
 
17
36
  def packaged_size(sku)
18
- # Takes an sku and returns the size
19
- result = sku[/(L\d{2}|U\d{2}|B\d{3}|C\d{3}|M\d{3}|P\d{2})/]
37
+ size_regex = /-(K?[LUP]\d{2}|[BCM]\d{3})/
38
+ match = sku.match(size_regex)
39
+ raise "Cannot determine packaged size for SKU: #{sku}" if match.nil? || match.captures.empty?
40
+
41
+ # this is everything inside the (..) in the regex (everything except the leading '-')
42
+ result = match.captures[0]
43
+ # HACK! for legacy reasons, we don't consider K to be part of the size.
44
+ result.gsub!(/^K/, '')
45
+
20
46
  if result == "B502"
21
47
  result = "B306"
22
48
  end
@@ -24,46 +50,77 @@ module Seshbot
24
50
  result = "B312"
25
51
  end
26
52
  raise "Cannot determine packaged size for SKU: #{sku}" if result.nil?
27
- result.gsub!(/^C/, 'B')
53
+
28
54
  result
29
55
  end
30
56
 
31
- def bundle_items(items, fulfilled_at: nil)
32
- if fulfilled_at.present?
33
- bundling_start_date = DateTime.new(2020, 1, 14, 8, 0, 0, Time.new.zone)
34
- result = items.map { |item| { "sku" => item.sku, "quantity" => item.quantity, "price" => item.price } }
35
- return result if fulfilled_at < bundling_start_date # TODO remove bundling start date method
57
+ def bundle_items(recipes, items, fulfilled_at: nil)
58
+ logger.debug "***** BUNDLING"
59
+ logger.debug "Bundling Items: #{items}. Fulfilled at: #{fulfilled_at || '(no fulfillment date)'}"
60
+ if !fulfilled_at.nil? && fulfilled_at < DateTime.new(2020, 1, 14, 8, 0, 0, Time.new.zone)
61
+ return items.map { |item| { 'sku' => item.sku, 'quantity' => item.quantity, 'price' => item.price } }
36
62
  end
37
63
 
38
- # get a hash of {pack_6: 5, pack_12: 1}
39
- product_type_counts = count_product_types(items)
64
+ # get the cans first, and separate them out
65
+ cans = filter_by_sku(items, '-C')
66
+ unless cans.empty?
67
+ product_type_counts = count_product_types(cans)
68
+ product_type_counts = unpack(recipes, product_type_counts)
69
+ product_type_counts = pack(recipes, product_type_counts)
70
+ new_can_items = product_type_counts.map { |k, v| { 'sku' => "BUND-#{k}", 'quantity' => v, 'price' => 0 } }
71
+ # separate out all the C324s
72
+ separated_c324s = filter_by_sku(new_can_items, '-C324')
73
+ logger.debug "Cans: #{cans}"
74
+ logger.debug " - Removed C324s: #{separated_c324s}"
40
75
 
41
- # dismantle packages into individual units (e.g., {pack_6: 7})
42
- product_type_counts = unpack(product_type_counts)
76
+ remaining_cans = filter_by_sku(new_can_items, '-C324', inverse: true)
77
+ logger.debug " - Remaining skus: #{remaining_cans}"
78
+ end
43
79
 
80
+ # merge the remaining with the original leftover
81
+ non_cans = filter_by_sku(items, '-C', inverse: true)
82
+ remaining_cans ||= []
83
+ remaining_items = non_cans + remaining_cans
84
+ logger.debug "Substituting:"
85
+ logger.debug " - remaining items: #{remaining_items}"
86
+
87
+ # substitute C's for B's
88
+ substitute_sku(remaining_items, '-C', '-B')
89
+ logger.debug " - skus updated to: #{remaining_items}"
90
+
91
+ # get a hash of {pack_6: 5, pack_12: 1}
92
+ product_type_counts = count_product_types(remaining_items)
93
+ # dismantle packages into individual units (e.g., {pack_6: 7})
94
+ product_type_counts = unpack(recipes, product_type_counts)
44
95
  # repackage into the 'best' packaging we can figure out (e.g., {pack_12: 2})
45
- product_type_counts = pack(product_type_counts)
96
+ product_type_counts = pack(recipes, product_type_counts)
97
+ new_remaining_items = product_type_counts.map { |k,v| { "sku" => "BUND-#{k}", "quantity" => v, "price" => 0} }
46
98
 
47
- new_line_items = product_type_counts.map { |k,v| { "sku" => "BUND-#{k}", "quantity" => v, "price" => 0} }
48
- new_line_items
99
+ separated_c324s ||= []
100
+ merged_items = new_remaining_items + separated_c324s
101
+ logger.debug "Bundled result:"
102
+ logger.debug " - IN: #{items}"
103
+ logger.debug " - OUT: #{merged_items}"
104
+ merged_items
49
105
  end
50
106
 
51
107
 
52
- def unpack(product_type_counts)
108
+ def unpack(recipes, product_type_counts)
53
109
  final_result = product_type_counts
54
110
 
55
111
  while true
56
- new_result = pack_single_step(final_result, unpacking: true)
112
+ new_result = pack_single_step(recipes, final_result, unpacking: true)
57
113
  break if new_result == final_result
114
+
58
115
  final_result = new_result
59
116
  end
60
117
  final_result
61
118
  end
62
119
 
63
- def pack_single_step(product_type_counts, unpacking:)
120
+ def pack_single_step(recipes, product_type_counts, unpacking:)
64
121
  result = Hash.new(0)
65
122
  product_type_counts.each do |sku_fragment, qty|
66
- recipe = Recipe::find_best_recipe(sku_fragment, qty, unpacking: unpacking)
123
+ recipe = Recipe::find_best_recipe(recipes, sku_fragment, qty, unpacking: unpacking)
67
124
  if recipe.nil?
68
125
  result[sku_fragment] += qty
69
126
  next
@@ -78,12 +135,12 @@ module Seshbot
78
135
  end
79
136
 
80
137
 
81
- def pack(product_type_counts)
138
+ def pack(recipes, product_type_counts)
82
139
  final_result = product_type_counts
83
140
 
84
141
  # keep trying to 'pack' until it stabilises (e.g., 6x6pack => 1x24pack+2x6pack => 1x24pack+1x12pack)
85
142
  while true
86
- new_result = pack_single_step(final_result, unpacking: false)
143
+ new_result = pack_single_step(recipes, final_result, unpacking: false)
87
144
  # no changes, break out
88
145
  break if new_result == final_result
89
146
  final_result = new_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.7.0"
3
+ VERSION = '0.8.5'
4
4
  end
5
5
  end
@@ -1,20 +1,20 @@
1
1
  require_relative 'lib/seshbot/packing/version'
2
2
 
3
3
  Gem::Specification.new do |spec|
4
- spec.name = "seshbot-packing"
4
+ spec.name = 'seshbot-packing'
5
5
  spec.version = Seshbot::Packing::VERSION
6
- spec.authors = ["Shaun"]
7
- spec.email = ["shaun@brewkeeper.com.au"]
8
-
6
+ spec.authors = ['Shaun']
7
+ spec.email = ['shaun@brewkeeper.com.au']
8
+ spec.license = 'MIT'
9
9
  spec.summary = %q{Receives, calculates, returns skus with new quantities}
10
- spec.homepage = "https://gitlab.com/brewkeeper/seshbot-packing"
10
+ spec.homepage = 'https://gitlab.com/brewkeeper/seshbot-packing'
11
11
  spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
12
12
 
13
13
  # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
16
  spec.metadata["source_code_uri"] = "https://gitlab.com/brewkeeper/seshbot-packing"
17
- spec.metadata["changelog_uri"] = "https://gitlab.com/brewkeeper/seshbot-packing/~blob/master/CHANGELOG.md"
17
+ spec.metadata["changelog_uri"] = "https://gitlab.com/brewkeeper/seshbot-packing/-/blob/master/CHANGELOG.md"
18
18
 
19
19
  # Specify which files should be added to the gem when it is released.
20
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
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.7.0
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaun
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-03 00:00:00.000000000 Z
11
+ date: 2021-03-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -19,6 +19,7 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - ".gitignore"
21
21
  - ".travis.yml"
22
+ - CHANGELOG.md
22
23
  - CODE_OF_CONDUCT.md
23
24
  - Gemfile
24
25
  - Gemfile.lock
@@ -32,11 +33,12 @@ files:
32
33
  - lib/seshbot/packing/version.rb
33
34
  - seshbot-packing.gemspec
34
35
  homepage: https://gitlab.com/brewkeeper/seshbot-packing
35
- licenses: []
36
+ licenses:
37
+ - MIT
36
38
  metadata:
37
39
  homepage_uri: https://gitlab.com/brewkeeper/seshbot-packing
38
40
  source_code_uri: https://gitlab.com/brewkeeper/seshbot-packing
39
- changelog_uri: https://gitlab.com/brewkeeper/seshbot-packing/~blob/master/CHANGELOG.md
41
+ changelog_uri: https://gitlab.com/brewkeeper/seshbot-packing/-/blob/master/CHANGELOG.md
40
42
  post_install_message:
41
43
  rdoc_options: []
42
44
  require_paths: