order_optimizer 0.2.0 → 0.5.0

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: 21b6b1f9358d26572d0f3fe4d2a237c2da2a217bbb5f2d241d7a98683a9a43e1
4
+ data.tar.gz: 29987d3e12a263c8f18acdcf6c1972dcfa91204ece002e1a778ccc72a38a2843
5
5
  SHA512:
6
- metadata.gz: 958f1f99429e00465cdc9a562dff1717da0b145827430704766756626444559e25f1f0b615c7f19cb274bc1783e33fb2eb3ca34b8e4d114e08af5f01f75c05f2
7
- data.tar.gz: 0cfb7dc00f8eacff31bff96cfedee003427e9e9e4677f8a382f7aa28e0148de5ca902ca576697aeba54c3b28a19a3302f451fb0944dc96464908742a9328287f
6
+ metadata.gz: 32b08adac0ba68f5133e60506b72a2f969d2e5617fd02ab16e922a05088e0afb81294041d6cbf6762dc70e8877a522733dc6160bb5583a30f744d67668efbee7
7
+ data.tar.gz: e8808b19221863663091640750d3765fccbf9dea0a4191f8cb9b1d13c46f49a3fa47f7ba8f906efaaa7bf5f3a6f7f13873ccdf19a59fcd32e81025b409676300
@@ -0,0 +1,63 @@
1
+ version: 2.1
2
+ jobs:
3
+ test:
4
+ docker:
5
+ - image: circleci/ruby:3.0
6
+
7
+ working_directory: ~/repo
8
+
9
+ steps:
10
+ - checkout
11
+
12
+ - restore_cache:
13
+ keys:
14
+ - v1-dependencies-{{ checksum "order_optimizer.gemspec" }}
15
+ # fallback to using the latest cache if no exact match is found
16
+ - v1-dependencies-
17
+
18
+ - run:
19
+ name: install dependencies
20
+ command: bundle install --jobs=4 --retry=3 --path vendor/bundle
21
+
22
+ - save_cache:
23
+ paths:
24
+ - ./vendor/bundle
25
+ key: v1-dependencies-{{ checksum "order_optimizer.gemspec" }}
26
+
27
+ - run:
28
+ name: run tests
29
+ command: bundle exec rake
30
+
31
+ publish:
32
+ docker:
33
+ - image: circleci/ruby:3.0
34
+ working_directory: ~/repo
35
+ steps:
36
+ - checkout
37
+ - run:
38
+ name: Build package
39
+ command: gem build order_optimizer.gemspec
40
+ - run:
41
+ name: Push package
42
+ command: |
43
+ VERSION=$(ruby -r "./lib/order_optimizer/version.rb" -e "print OrderOptimizer::VERSION")
44
+ gem push order_optimizer-${VERSION}.gem
45
+
46
+ workflows:
47
+ default:
48
+ jobs:
49
+ - test:
50
+ filters:
51
+ tags:
52
+ only: /.*/
53
+ branches:
54
+ only: /.*/
55
+ - publish:
56
+ context:
57
+ - rubygems-push
58
+ requires: [test]
59
+ filters:
60
+ tags:
61
+ only: /^v.*/
62
+ branches:
63
+ ignore: /.*/
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -1,7 +1,29 @@
1
1
  # OrderOptimizer Gem Changelog
2
2
 
3
+ ## 0.5.0 (2021-04-15)
4
+ - Add `maximum_quantity` for SKUs
5
+
6
+ ## 0.4.1 (2021-02-15)
7
+
8
+ - Fix issue with floating point conversion [#3]
9
+
10
+ ## 0.4.0 (2020-12-11)
11
+
12
+ - Add `possible_orders` method to find all possible orders
13
+ - Fix issues with zero remainders on calculations
14
+
15
+ ## 0.3.0 (2020-11-23)
16
+
17
+ - Add `cheapest_exact_order` method to optimize an order with an exact amount
18
+
19
+ ## 0.2.1 (2019-12-18)
20
+
21
+ - Improve result for mixed catalogs with discounts and multiple pack sizes
22
+
3
23
  ## 0.2.0 (2019-12-16)
4
24
 
5
25
  - Add support for discounts when ordering a minimum quantity
6
26
 
7
27
  ## 0.1.0 First release
28
+
29
+ [#3]: https://github.com/zaikio/order_optimizer/pull/3
data/README.md CHANGED
@@ -56,7 +56,23 @@ cheapest_order_for_947_units.total # => 3_900.00
56
56
  cheapest_order_for_947_units.skus # => { '1000-pack' => 1 }
57
57
  ```
58
58
 
59
- It is possible to define dicount prices a minimum quantity:
59
+ You can also optimize your order for exact order amounts
60
+
61
+ ```ruby
62
+ # Initialize the optimize with a catalog
63
+ order_optimizer = OrderOptimizer.new(
64
+ '1-pack' => { quantity: 1, price_per_unit: 9 },
65
+ '10-pack' => { quantity: 10, price_per_unit: 5 },
66
+ )
67
+
68
+ # Pick the cheapest order that includes the exact required quantity
69
+ cheapest_exact_order_for_58_units = order_optimizer.cheapest_exact_order(required_qty: 56)
70
+ cheapest_exact_order_for_58_units.quantity # => 58
71
+ cheapest_exact_order_for_58_units.total # => 322
72
+ cheapest_exact_order_for_58_units.skus # => { '10-pack' => 5, '1-pack' => 8 }
73
+ ```
74
+
75
+ It is possible to define discount prices with a minimum quantity:
60
76
 
61
77
  ```ruby
62
78
  order_optimizer = OrderOptimizer.new(
@@ -84,15 +100,52 @@ order_optimizer.cheapest_order(required_qty: 29).skus
84
100
  #=> { '20-pack' => 1, '10-discount' => 10 }
85
101
  ```
86
102
 
103
+ It is also possible to define discount prices with a maximum quantity:
104
+ ```ruby
105
+ order_optimizer = OrderOptimizer.new(
106
+ '1-pack' => { quantity: 1, price_per_unit: 9 },
107
+ '10-pack' => { quantity: 10, price_per_unit: 8, max_quantity: 20 },
108
+ )
109
+
110
+ order_optimizer.cheapest_order(required_qty: 30).skus
111
+ #=> { '10-pack' => 2, '1-pack' => 10 }
112
+ ```
113
+
114
+ If you're just interested in all possible order combinations:
115
+
116
+ ```ruby
117
+ # Initialize the optimize with a catalog
118
+ order_optimizer = OrderOptimizer.new(
119
+ '1-pack' => { quantity: 1, price_per_unit: 9 },
120
+ '10-pack' => { quantity: 10, price_per_unit: 5 },
121
+ )
122
+
123
+ order_optimizer.possible_orders(required_qty: 11).size
124
+ #=> 3
125
+ order_optimizer.possible_orders(required_qty: 10).map(&:skus)
126
+ #=> [{"10-pack"=>1, "1-pack"=>1}, {"1-pack"=>11}, {"10-pack"=>2}]
127
+ ```
128
+
87
129
  ## Development
88
130
 
89
131
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
90
132
 
91
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
92
-
93
133
  ## Contributing
94
134
 
95
- Bug reports and pull requests are welcome on GitHub at https://github.com/crispymtn/order_optimizer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
135
+ Bug reports and pull requests are welcome on GitHub at
136
+ https://github.com/zaikio/order_optimizer. This project is intended to be a safe,
137
+ welcoming space for collaboration, and contributors are expected to adhere to the
138
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
139
+
140
+ - Make your changes and submit a pull request for them
141
+ - Make sure to update `CHANGELOG.md`
142
+
143
+ To release a new version of the gem:
144
+ - Update the version in `lib/order_optimizer/version.rb`
145
+ - Update `CHANGELOG.md` to include the new version and its release date
146
+ - Commit and push your changes
147
+ - Create a [new release on GitHub](https://github.com/zaikio/order_optimizer/releases/new)
148
+ - CircleCI will build the Gem package and push it Rubygems for you
96
149
 
97
150
  ## License
98
151
 
@@ -100,4 +153,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
100
153
 
101
154
  ## Code of Conduct
102
155
 
103
- Everyone interacting in the OrderOptimizer project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/crispymtn/order_optimizer/blob/master/CODE_OF_CONDUCT.md).
156
+ Everyone interacting in the OrderOptimizer project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zaikio/order_optimizer/blob/master/CODE_OF_CONDUCT.md).
@@ -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,63 @@ 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)
13
+ find_possible_orders(skus: @catalog.skus, required_qty: required_qty).min_by(&:total) ||
14
+ OrderOptimizer::Order.new(required_qty: required_qty)
15
+ end
16
+
17
+ def cheapest_exact_order(required_qty:)
18
+ find_possible_orders(skus: @catalog.skus, required_qty: required_qty).select(&:exact?).min_by(&:total) ||
19
+ OrderOptimizer::Order.new(required_qty: required_qty)
20
+ end
17
21
 
18
- orders.min_by(&:total) || OrderOptimizer::Order.new
22
+ def possible_orders(required_qty:)
23
+ find_possible_orders(skus: @catalog.skus, required_qty: required_qty).sort_by(&:total)
19
24
  end
20
25
 
21
26
  private
22
27
 
23
- def possible_orders(required_qty:, skus:)
24
- [].tap do |orders|
25
- order = OrderOptimizer::Order.new
28
+ def find_possible_orders(required_qty:, skus:)
29
+ return [] if required_qty < 1 || skus.empty?
30
+
31
+ orders = []
32
+
33
+ skus.each do |sku|
34
+ orders.reject(&:complete?).each do |order|
35
+ count, remainder, skip_increase = count_and_remainder_for_sku(order.missing_qty, sku)
26
36
 
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)
37
+ orders << order.dup.add(sku, count: count) unless count.zero?
38
+ orders << order.dup.add(sku, count: count + 1) unless remainder.zero? || skip_increase
39
+ end
30
40
 
31
- order.add(sku, count: count) unless count.zero?
41
+ count, remainder, skip_increase = count_and_remainder_for_sku(required_qty, sku)
32
42
 
33
- orders << (remainder.positive? ? order.dup.add(sku) : order.dup)
43
+ unless count.zero?
44
+ orders << OrderOptimizer::Order.new(required_qty: required_qty).add(sku, count: count)
45
+ end
34
46
 
35
- order = OrderOptimizer::Order.new if sku.min_quantity
47
+ unless remainder.zero? || skip_increase
48
+ orders << OrderOptimizer::Order.new(required_qty: required_qty).add(sku, count: count + 1)
36
49
  end
37
50
  end
51
+
52
+ orders.select(&:complete?)
38
53
  end
39
54
 
40
- def adjustment_for_skus_with_min_quantity(sku, count, remainder)
41
- units = count * sku.quantity
55
+ def count_and_remainder_for_sku(quantity, sku)
56
+ count, remainder = quantity.divmod(sku.quantity)
42
57
 
43
- return count, remainder if sku.min_quantity.nil? || units >= sku.min_quantity
58
+ if sku.max_quantity && count * sku.quantity > sku.max_quantity
59
+ new_count = (sku.max_quantity / sku.quantity).ceil
60
+ remainder = (count - new_count) * sku.quantity
44
61
 
45
- delta = ((sku.min_quantity - units).to_f / sku.quantity).ceil
46
- [count + delta, remainder - (delta * sku.quantity)]
62
+ [new_count, [remainder, 0].max, true]
63
+ elsif sku.min_quantity && count * sku.quantity < sku.min_quantity
64
+ new_count = (sku.min_quantity / sku.quantity).ceil
65
+ remainder -= (new_count - count) * sku.quantity
66
+
67
+ [new_count, [remainder, 0].max, false]
68
+ else
69
+ [count, remainder, false]
70
+ end
47
71
  end
48
72
  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,30 @@ 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
26
+
27
+ def exact?
28
+ @required_qty == quantity
29
+ end
17
30
  end
18
31
  end
@@ -1,25 +1,24 @@
1
1
  class OrderOptimizer
2
2
  class Sku
3
- attr_reader :id, :quantity, :price_per_unit, :price_per_sku, :min_quantity
3
+ attr_reader :id, :quantity, :price_per_unit, :price_per_sku, :min_quantity, :max_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, max_quantity: nil)
6
6
  @id = id
7
- @quantity = quantity
8
- @price_per_unit = BigDecimal(price_per_unit, 2)
9
- @price_per_sku = quantity * price_per_unit
10
- @min_quantity = min_quantity
11
- end
7
+ @quantity = BigDecimal(quantity, 2)
8
+ @min_quantity = BigDecimal(min_quantity, 2) if min_quantity
9
+ @max_quantity = BigDecimal(max_quantity, 2) if max_quantity
10
+
11
+ if min_quantity && max_quantity && (min_quantity > max_quantity)
12
+ raise ArgumentError, "min_quantity can't be larger than max_quantity"
13
+ end
12
14
 
13
- private
15
+ raise ArgumentError, ':price_per_sku or :price_per_unit must be set' unless price_per_unit || price_per_sku
14
16
 
15
- def set_min_quantity(min_qty)
16
- return unless min_qty
17
+ @price_per_unit = BigDecimal(price_per_unit, 2) if price_per_unit
18
+ @price_per_unit ||= BigDecimal(price_per_sku, 2) / quantity
17
19
 
18
- if min_quantity.modulo(quantity).zero?
19
- @min_quantity = min_qty
20
- else
21
- raise ':min_quantity must be a multiple of :quantity'
22
- end
20
+ @price_per_sku = BigDecimal(price_per_sku, 2) if price_per_sku
21
+ @price_per_sku ||= quantity * price_per_unit
23
22
  end
24
23
  end
25
24
  end
@@ -1,3 +1,3 @@
1
1
  class OrderOptimizer
2
- VERSION = "0.2.0"
2
+ VERSION = "0.5.0".freeze
3
3
  end
@@ -6,14 +6,14 @@ Gem::Specification.new do |spec|
6
6
  spec.name = "order_optimizer"
7
7
  spec.version = OrderOptimizer::VERSION
8
8
 
9
- spec.authors = ["crispymtn", "Martin Spickermann"]
10
- spec.email = ["op@crispymtn.com", "spickermann@gmail.com"]
11
- spec.homepage = "https://github.com/crispymtn/order_optimizer"
9
+ spec.authors = ["crispymtn", "Martin Spickermann", "Maurice Vogel"]
10
+ spec.email = ["op@zaikio.com", "spickermann@gmail.com"]
11
+ spec.homepage = "https://github.com/zaikio/order_optimizer"
12
12
  spec.license = "MIT"
13
13
  spec.summary = "Helps to optimize orders if the goods are offered in different pack sizes and in different discount levels."
14
14
 
15
15
 
16
- spec.metadata["changelog_uri"] = "https://github.com/crispymtn/order_optimizer/blob/master/CHANGELOG.md"
16
+ spec.metadata["changelog_uri"] = "https://github.com/zaikio/order_optimizer/blob/master/CHANGELOG.md"
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
19
19
 
metadata CHANGED
@@ -1,15 +1,16 @@
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.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - crispymtn
8
8
  - Martin Spickermann
9
+ - Maurice Vogel
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2019-12-16 00:00:00.000000000 Z
13
+ date: 2021-04-15 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: bundler
@@ -55,18 +56,18 @@ dependencies:
55
56
  version: '0'
56
57
  description:
57
58
  email:
58
- - op@crispymtn.com
59
+ - op@zaikio.com
59
60
  - spickermann@gmail.com
60
61
  executables: []
61
62
  extensions: []
62
63
  extra_rdoc_files: []
63
64
  files:
65
+ - ".circleci/config.yml"
64
66
  - ".gitignore"
65
67
  - ".travis.yml"
66
68
  - CHANGELOG.md
67
69
  - CODE_OF_CONDUCT.md
68
70
  - Gemfile
69
- - Gemfile.lock
70
71
  - LICENSE.txt
71
72
  - README.md
72
73
  - Rakefile
@@ -78,13 +79,13 @@ files:
78
79
  - lib/order_optimizer/sku.rb
79
80
  - lib/order_optimizer/version.rb
80
81
  - order_optimizer.gemspec
81
- homepage: https://github.com/crispymtn/order_optimizer
82
+ homepage: https://github.com/zaikio/order_optimizer
82
83
  licenses:
83
84
  - MIT
84
85
  metadata:
85
- changelog_uri: https://github.com/crispymtn/order_optimizer/blob/master/CHANGELOG.md
86
- homepage_uri: https://github.com/crispymtn/order_optimizer
87
- source_code_uri: https://github.com/crispymtn/order_optimizer
86
+ changelog_uri: https://github.com/zaikio/order_optimizer/blob/master/CHANGELOG.md
87
+ homepage_uri: https://github.com/zaikio/order_optimizer
88
+ source_code_uri: https://github.com/zaikio/order_optimizer
88
89
  post_install_message:
89
90
  rdoc_options: []
90
91
  require_paths:
@@ -100,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
101
  - !ruby/object:Gem::Version
101
102
  version: '0'
102
103
  requirements: []
103
- rubygems_version: 3.0.6
104
+ rubygems_version: 3.2.15
104
105
  signing_key:
105
106
  specification_version: 4
106
107
  summary: Helps to optimize orders if the goods are offered in different pack sizes
data/Gemfile.lock DELETED
@@ -1,22 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- order_optimizer (0.1.0)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- minitest (5.13.0)
10
- rake (13.0.1)
11
-
12
- PLATFORMS
13
- ruby
14
-
15
- DEPENDENCIES
16
- bundler
17
- minitest
18
- order_optimizer!
19
- rake
20
-
21
- BUNDLED WITH
22
- 2.0.2