order_optimizer 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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