order_optimizer 0.2.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3dec3fd3cab38f6a56c1fa13e3ee398e4b196c1c992cdf6f4c7e87c2c19e2f1
4
- data.tar.gz: 9cd52e4ae25d980de2b8b4ba1988bbe42dac0dd686d0d1c7f23584c7889cb3e8
3
+ metadata.gz: c7436b34e3961c7cb835d33eb4212ce823fc050d362af7b3297eb80a2a5dda77
4
+ data.tar.gz: 366e5d4ba6d0fb9772a982ec0aeccf6aba82cc3e8bb79237954afdfebc42a5da
5
5
  SHA512:
6
- metadata.gz: 958f1f99429e00465cdc9a562dff1717da0b145827430704766756626444559e25f1f0b615c7f19cb274bc1783e33fb2eb3ca34b8e4d114e08af5f01f75c05f2
7
- data.tar.gz: 0cfb7dc00f8eacff31bff96cfedee003427e9e9e4677f8a382f7aa28e0148de5ca902ca576697aeba54c3b28a19a3302f451fb0944dc96464908742a9328287f
6
+ metadata.gz: cf15bc0ee13c1f1c244b9d5a54b7271f1f6fee150305411bab9f18353d80f701e148c86eeda665ab555222f4a0cd10f5a8370d18910f190dc886080fc0883e03
7
+ data.tar.gz: 0aa94e6ce45593754e611376e9a29389caa3ae1d167f3406d7ef232a0906918802dc6515bff0f63cc3d8830d5d863c1ed316f6197457719c2aa974afc7a3051b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # OrderOptimizer Gem Changelog
2
2
 
3
+ ## 0.2.1 (2019-12-18)
4
+
5
+ - Improve result for mixed catalogs with discounts and multiple pack sizes
6
+
3
7
  ## 0.2.0 (2019-12-16)
4
8
 
5
9
  - Add support for discounts when ordering a minimum quantity
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- order_optimizer (0.1.0)
4
+ order_optimizer (0.2.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,8 +1,8 @@
1
1
  require 'bigdecimal'
2
2
 
3
- require "order_optimizer/catalog"
4
- require "order_optimizer/order"
5
- require "order_optimizer/version"
3
+ require 'order_optimizer/catalog'
4
+ require 'order_optimizer/order'
5
+ require 'order_optimizer/version'
6
6
 
7
7
  class OrderOptimizer
8
8
  def initialize(skus)
@@ -10,39 +10,47 @@ class OrderOptimizer
10
10
  end
11
11
 
12
12
  def cheapest_order(required_qty:)
13
- orders =
14
- possible_orders(skus: @catalog.skus_without_min_quantities, required_qty: required_qty) +
15
- possible_orders(skus: @catalog.skus_with_min_quantities, required_qty: required_qty) +
16
- possible_orders(skus: @catalog.skus, required_qty: required_qty)
17
-
18
- orders.min_by(&:total) || OrderOptimizer::Order.new
13
+ possible_orders(skus: @catalog.skus, required_qty: required_qty)
14
+ .min_by(&:total) || OrderOptimizer::Order.new(required_qty: required_qty)
19
15
  end
20
16
 
21
17
  private
22
18
 
23
19
  def possible_orders(required_qty:, skus:)
24
- [].tap do |orders|
25
- order = OrderOptimizer::Order.new
20
+ return [] if required_qty < 1 || skus.empty?
21
+
22
+ orders = []
26
23
 
27
- while required_qty.positive? && (sku = skus.shift)
28
- count, remainder = (required_qty - order.quantity).divmod(sku.quantity)
29
- count, remainder = adjustment_for_skus_with_min_quantity(sku, count, remainder)
24
+ skus.each do |sku|
25
+ orders.reject(&:complete?).each do |order|
26
+ count, remainder = count_and_remainder_for_sku(order.missing_qty, sku)
30
27
 
31
- order.add(sku, count: count) unless count.zero?
28
+ orders << order.dup.add(sku, count: count) unless count.zero?
29
+ orders << order.dup.add(sku, count: count + 1) if remainder
30
+ end
32
31
 
33
- orders << (remainder.positive? ? order.dup.add(sku) : order.dup)
32
+ count, remainder = count_and_remainder_for_sku(required_qty, sku)
34
33
 
35
- order = OrderOptimizer::Order.new if sku.min_quantity
34
+ unless count.zero?
35
+ orders << OrderOptimizer::Order.new(required_qty: required_qty).add(sku, count: count)
36
+ end
37
+ if remainder
38
+ orders << OrderOptimizer::Order.new(required_qty: required_qty).add(sku, count: count + 1)
36
39
  end
37
40
  end
38
- end
39
41
 
40
- def adjustment_for_skus_with_min_quantity(sku, count, remainder)
41
- units = count * sku.quantity
42
+ orders.select(&:complete?)
43
+ end
42
44
 
43
- return count, remainder if sku.min_quantity.nil? || units >= sku.min_quantity
45
+ def count_and_remainder_for_sku(quantity, sku)
46
+ count, remainder = quantity.divmod(sku.quantity)
44
47
 
45
- delta = ((sku.min_quantity - units).to_f / sku.quantity).ceil
46
- [count + delta, remainder - (delta * sku.quantity)]
48
+ if sku.min_quantity && count * sku.quantity < sku.min_quantity
49
+ new_count = (sku.min_quantity.to_f / sku.quantity).ceil
50
+ remainder -= (new_count - count) * sku.quantity
51
+ [new_count, [remainder, 0].max]
52
+ else
53
+ [count, remainder]
54
+ end
47
55
  end
48
56
  end
@@ -9,13 +9,5 @@ class OrderOptimizer
9
9
  def skus
10
10
  @skus.dup
11
11
  end
12
-
13
- def skus_without_min_quantities
14
- @skus.reject(&:min_quantity)
15
- end
16
-
17
- def skus_with_min_quantities
18
- @skus.select(&:min_quantity)
19
- end
20
12
  end
21
13
  end
@@ -2,17 +2,26 @@ class OrderOptimizer
2
2
  class Order
3
3
  attr_reader :quantity, :total, :skus
4
4
 
5
- def initialize
6
- @quantity = 0
7
- @total = 0
8
- @skus = {}
5
+ def initialize(required_qty:)
6
+ @quantity = 0
7
+ @required_qty = required_qty
8
+ @total = 0
9
+ @skus = {}
9
10
  end
10
11
 
11
12
  def add(sku, count: 1)
12
13
  @quantity += count * sku.quantity
13
14
  @total += count * sku.price_per_sku
14
- @skus = skus.merge(sku.id => count) { |identifier, current, plus| current + plus }
15
+ @skus = skus.merge(sku.id => count) { |_identifier, current, plus| current + plus }
15
16
  self
16
17
  end
18
+
19
+ def missing_qty
20
+ [@required_qty - quantity, 0].max
21
+ end
22
+
23
+ def complete?
24
+ missing_qty.zero?
25
+ end
17
26
  end
18
27
  end
@@ -2,24 +2,20 @@ class OrderOptimizer
2
2
  class Sku
3
3
  attr_reader :id, :quantity, :price_per_unit, :price_per_sku, :min_quantity
4
4
 
5
- def initialize(id, quantity:, price_per_unit:, min_quantity: nil)
5
+ def initialize(id, quantity:, price_per_sku: nil, price_per_unit: nil, min_quantity: nil)
6
6
  @id = id
7
7
  @quantity = quantity
8
- @price_per_unit = BigDecimal(price_per_unit, 2)
9
- @price_per_sku = quantity * price_per_unit
10
8
  @min_quantity = min_quantity
11
- end
12
-
13
- private
14
9
 
15
- def set_min_quantity(min_qty)
16
- return unless min_qty
10
+ @price_per_unit = BigDecimal(price_per_unit, 2) if price_per_unit
11
+ @price_per_sku = BigDecimal(price_per_sku, 2) if price_per_sku
17
12
 
18
- if min_quantity.modulo(quantity).zero?
19
- @min_quantity = min_qty
20
- else
21
- raise ':min_quantity must be a multiple of :quantity'
13
+ unless price_per_unit || price_per_sku
14
+ raise ':price_per_sku or :price_per_unit must be set'
22
15
  end
16
+
17
+ @price_per_unit ||= price_per_sku.to_f / quantity
18
+ @price_per_sku ||= quantity * price_per_unit
23
19
  end
24
20
  end
25
21
  end
@@ -1,3 +1,3 @@
1
1
  class OrderOptimizer
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: order_optimizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - crispymtn
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-12-16 00:00:00.000000000 Z
12
+ date: 2019-12-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler