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 +4 -4
- data/.circleci/config.yml +63 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +22 -0
- data/README.md +58 -5
- data/lib/order_optimizer.rb +46 -22
- data/lib/order_optimizer/catalog.rb +0 -8
- data/lib/order_optimizer/order.rb +18 -5
- data/lib/order_optimizer/sku.rb +14 -15
- data/lib/order_optimizer/version.rb +1 -1
- data/order_optimizer.gemspec +4 -4
- metadata +10 -9
- data/Gemfile.lock +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21b6b1f9358d26572d0f3fe4d2a237c2da2a217bbb5f2d241d7a98683a9a43e1
|
4
|
+
data.tar.gz: 29987d3e12a263c8f18acdcf6c1972dcfa91204ece002e1a778ccc72a38a2843
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
-
|
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
|
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/
|
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).
|
data/lib/order_optimizer.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
24
|
-
[]
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
41
|
+
count, remainder, skip_increase = count_and_remainder_for_sku(required_qty, sku)
|
32
42
|
|
33
|
-
|
43
|
+
unless count.zero?
|
44
|
+
orders << OrderOptimizer::Order.new(required_qty: required_qty).add(sku, count: count)
|
45
|
+
end
|
34
46
|
|
35
|
-
|
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
|
41
|
-
|
55
|
+
def count_and_remainder_for_sku(quantity, sku)
|
56
|
+
count, remainder = quantity.divmod(sku.quantity)
|
42
57
|
|
43
|
-
|
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
|
-
|
46
|
-
|
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
|
@@ -2,17 +2,30 @@ class OrderOptimizer
|
|
2
2
|
class Order
|
3
3
|
attr_reader :quantity, :total, :skus
|
4
4
|
|
5
|
-
def initialize
|
6
|
-
@quantity
|
7
|
-
@
|
8
|
-
@
|
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
|
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
|
data/lib/order_optimizer/sku.rb
CHANGED
@@ -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
|
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
|
-
@
|
9
|
-
@
|
10
|
-
|
11
|
-
|
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
|
-
|
15
|
+
raise ArgumentError, ':price_per_sku or :price_per_unit must be set' unless price_per_unit || price_per_sku
|
14
16
|
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
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
|
data/order_optimizer.gemspec
CHANGED
@@ -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@
|
11
|
-
spec.homepage = "https://github.com/
|
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/
|
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.
|
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:
|
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@
|
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/
|
82
|
+
homepage: https://github.com/zaikio/order_optimizer
|
82
83
|
licenses:
|
83
84
|
- MIT
|
84
85
|
metadata:
|
85
|
-
changelog_uri: https://github.com/
|
86
|
-
homepage_uri: https://github.com/
|
87
|
-
source_code_uri: https://github.com/
|
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.
|
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
|