order_optimizer 0.1.0 → 0.4.1

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: f0845a565bc344866e5e3d4725d28b890c9f3e4d57de36ee2789ee331f000168
4
- data.tar.gz: b825661a3746a8b7bf574c3938efb2b412ac81163223a4836be013dda983100e
3
+ metadata.gz: 5868b03e4f28cfec6001c9e53e61ffba0907c6d626d4ee950596bf4b1f154075
4
+ data.tar.gz: 739453efda8bcfe9e015f91a47b0cf8945d0c9270fe8f18dc6241a71c5d35e63
5
5
  SHA512:
6
- metadata.gz: 3103e356f667e9256ff3115d0381f05fa27906083232300c51833fcd5b15118fd4b4154542f07e781882a25a4de8f70b4604c8748c529f92a91d41af315391c4
7
- data.tar.gz: 965406b44c6006b4ac863cc2dd3d8fc0872fd87a08bb4fadc45954cd81e6a724f7143975985c439a83a4a9a9d0630df54113a2e9e018cd16282ea420eff23315
6
+ metadata.gz: eebd782f5bc6971227292700da42edb7c30a3bca2d19573f65f901311b708aa91fd422ad1ca51a17798d28351e89a020eb1857b7ff7671892874f9e67fbe1429
7
+ data.tar.gz: d4f80534e745ed6723468c7bf7b49ce5fd5d45fb6db915b756601aa7b00894d10550baeb16bba5da96f4bc24c01e5638ab7ab79ac07ca6eb0bb6576aa4648663
@@ -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,4 +1,26 @@
1
1
  # OrderOptimizer Gem Changelog
2
2
 
3
+ ## 0.4.1 (2021-02-15)
4
+
5
+ - Fix issue with floating point conversion [#3]
6
+
7
+ ## 0.4.0 (2020-12-11)
8
+
9
+ - Add `possible_orders` method to find all possible orders
10
+ - Fix issues with zero remainders on calculations
11
+
12
+ ## 0.3.0 (2020-11-23)
13
+
14
+ - Add `cheapest_exact_order` method to optimize an order with an exact amount
15
+
16
+ ## 0.2.1 (2019-12-18)
17
+
18
+ - Improve result for mixed catalogs with discounts and multiple pack sizes
19
+
20
+ ## 0.2.0 (2019-12-16)
21
+
22
+ - Add support for discounts when ordering a minimum quantity
3
23
 
4
24
  ## 0.1.0 First release
25
+
26
+ [#3]: https://github.com/zaikio/order_optimizer/pull/3
data/README.md CHANGED
@@ -11,7 +11,7 @@ Imagine a product can be ordered in different pack sizes and each pack size has
11
11
  | 10 | 6.00 |
12
12
  | 1 | 9.00 |
13
13
 
14
- In this example, it is quite obvious that it is cheaper buying one 10-pack instead of nine 1-packs when you need 9 units (because the 10-pack costs 60.00 but nine 1-packs would cost 81.00).
14
+ In this example, it is quite obvious that it is cheaper buying one 10-pack instead of nine 1-packs when you need 9 units (because the 10-pack costs 60.00 but nine 1-packs would cost 81.00).
15
15
 
16
16
  But what would be the cheapest combination when you need 946 units? Or 947?
17
17
 
@@ -56,15 +56,85 @@ 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
+ 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 dicount prices a minimum quantity:
76
+
77
+ ```ruby
78
+ order_optimizer = OrderOptimizer.new(
79
+ '1-pack' => { quantity: 1, price_per_unit: 9 },
80
+ '10-discount' => { quantity: 1, min_quantity: 10, price_per_unit: 8 },
81
+ '20-pack' => { quantity: 20, price_per_unit: 7 }
82
+ )
83
+
84
+ order_optimizer.cheapest_order(required_qty: 8).skus
85
+ #=> { '1-pack' => 8 }
86
+
87
+ order_optimizer.cheapest_order(required_qty: 9).skus
88
+ #=> { '10-discount' => 10 }
89
+
90
+ order_optimizer.cheapest_order(required_qty: 17).skus
91
+ #=> { '10-discount' => 17 }
92
+
93
+ order_optimizer.cheapest_order(required_qty: 18).skus
94
+ #=> { '20-pack' => 1 }
95
+
96
+ order_optimizer.cheapest_order(required_qty: 21).skus
97
+ #=> { '20-pack' => 1, '1-pack' => 1 }
98
+
99
+ order_optimizer.cheapest_order(required_qty: 29).skus
100
+ #=> { '20-pack' => 1, '10-discount' => 10 }
101
+ ```
102
+
103
+ If you're just interested in all possible order combinations:
104
+
105
+ ```ruby
106
+ # Initialize the optimize with a catalog
107
+ order_optimizer = OrderOptimizer.new(
108
+ '1-pack' => { quantity: 1, price_per_unit: 9 },
109
+ '10-pack' => { quantity: 10, price_per_unit: 5 },
110
+ )
111
+
112
+ order_optimizer.possible_orders(required_qty: 11).size
113
+ #=> 3
114
+ order_optimizer.possible_orders(required_qty: 10).map(&:skus)
115
+ #=> [{"10-pack"=>1, "1-pack"=>1}, {"1-pack"=>11}, {"10-pack"=>2}]
116
+ ```
117
+
59
118
  ## Development
60
119
 
61
120
  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.
62
121
 
63
- 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).
64
-
65
122
  ## Contributing
66
123
 
67
- 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.
124
+ Bug reports and pull requests are welcome on GitHub at
125
+ https://github.com/zaikio/order_optimizer. This project is intended to be a safe,
126
+ welcoming space for collaboration, and contributors are expected to adhere to the
127
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
128
+
129
+ - Make your changes and submit a pull request for them
130
+ - Make sure to update `CHANGELOG.md`
131
+
132
+ To release a new version of the gem:
133
+ - Update the version in `lib/order_optimizer/version.rb`
134
+ - Update `CHANGELOG.md` to include the new version and its release date
135
+ - Commit and push your changes
136
+ - Create a [new release on GitHub](https://github.com/zaikio/order_optimizer/releases/new)
137
+ - CircleCI will build the Gem package and push it Rubygems for you
68
138
 
69
139
  ## License
70
140
 
@@ -72,4 +142,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
72
142
 
73
143
  ## Code of Conduct
74
144
 
75
- 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).
145
+ 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,21 +10,57 @@ class OrderOptimizer
10
10
  end
11
11
 
12
12
  def cheapest_order(required_qty:)
13
- possible_orders(required_qty: required_qty)
14
- .min_by(&:total)
13
+ find_possible_orders(skus: @catalog.skus, required_qty: required_qty).min_by(&:total) ||
14
+ OrderOptimizer::Order.new(required_qty: required_qty)
15
15
  end
16
16
 
17
- private
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
18
21
 
19
22
  def possible_orders(required_qty:)
20
- [].tap do |orders|
21
- skus = @catalog.skus
22
- order = OrderOptimizer::Order.new
23
- while sku = skus.shift
24
- count, remainder = (required_qty - order.quantity).divmod(sku.quantity)
25
- order.add(sku, count: count) unless count.zero?
26
- orders << (remainder.zero? ? order.dup : order.dup.add(sku))
23
+ find_possible_orders(skus: @catalog.skus, required_qty: required_qty).sort_by(&:total)
24
+ end
25
+
26
+ private
27
+
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 = count_and_remainder_for_sku(order.missing_qty, sku)
36
+
37
+ orders << order.dup.add(sku, count: count) unless count.zero?
38
+ orders << order.dup.add(sku, count: count + 1) unless remainder.zero?
27
39
  end
40
+
41
+ count, remainder = count_and_remainder_for_sku(required_qty, sku)
42
+
43
+ unless count.zero?
44
+ orders << OrderOptimizer::Order.new(required_qty: required_qty).add(sku, count: count)
45
+ end
46
+
47
+ unless remainder.zero?
48
+ orders << OrderOptimizer::Order.new(required_qty: required_qty).add(sku, count: count + 1)
49
+ end
50
+ end
51
+
52
+ orders.select(&:complete?)
53
+ end
54
+
55
+ def count_and_remainder_for_sku(quantity, sku)
56
+ count, remainder = quantity.divmod(sku.quantity)
57
+
58
+ if sku.min_quantity && count * sku.quantity < sku.min_quantity
59
+ new_count = (sku.min_quantity / sku.quantity).ceil
60
+ remainder -= (new_count - count) * sku.quantity
61
+ [new_count, [remainder, 0].max]
62
+ else
63
+ [count, remainder]
28
64
  end
29
65
  end
30
66
  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,12 +1,21 @@
1
1
  class OrderOptimizer
2
2
  class Sku
3
- attr_reader :id, :quantity, :price_per_unit, :price_per_sku
3
+ attr_reader :id, :quantity, :price_per_unit, :price_per_sku, :min_quantity
4
4
 
5
- def initialize(id, quantity:, price_per_unit:)
5
+ def initialize(id, quantity:, price_per_sku: nil, price_per_unit: nil, min_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
7
+ @quantity = BigDecimal(quantity, 2)
8
+ @min_quantity = BigDecimal(min_quantity, 2) if min_quantity
9
+
10
+ unless price_per_unit || price_per_sku
11
+ raise ':price_per_sku or :price_per_unit must be set'
12
+ end
13
+
14
+ @price_per_unit = BigDecimal(price_per_unit, 2) if price_per_unit
15
+ @price_per_unit ||= BigDecimal(price_per_sku, 2) / quantity
16
+
17
+ @price_per_sku = BigDecimal(price_per_sku, 2) if price_per_sku
18
+ @price_per_sku ||= quantity * price_per_unit
10
19
  end
11
20
  end
12
21
  end
@@ -1,3 +1,3 @@
1
1
  class OrderOptimizer
2
- VERSION = "0.1.0"
2
+ VERSION = "0.4.1".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.1.0
4
+ version: 0.4.1
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-13 00:00:00.000000000 Z
13
+ date: 2021-02-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.3
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