feat: upgrade makeChanges to a more robust version
This commit is contained in:
parent
4c1be68f2a
commit
8a4f768957
1 changed files with 172 additions and 158 deletions
330
lib/bill-math.js
330
lib/bill-math.js
|
|
@ -1,23 +1,75 @@
|
||||||
const uuid = require('uuid')
|
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
const uuid = require('uuid')
|
||||||
|
|
||||||
// Custom algorith for two cassettes. For three or more denominations, we'll need
|
const MAX_AMOUNT_OF_SOLUTIONS = 10000
|
||||||
// to rethink this. Greedy algorithm fails to find *any* solution in some cases.
|
const MAX_BRUTEFORCE_ITERATIONS = 10000000
|
||||||
// Dynamic programming may be too inefficient for large amounts.
|
|
||||||
//
|
|
||||||
// We can either require canononical denominations for 3+, or try to expand
|
|
||||||
// this algorithm.
|
|
||||||
exports.makeChange = function makeChange (cassettes, amount) {
|
|
||||||
// Note: Everything here is converted to primitive numbers,
|
|
||||||
// since they're all integers, well within JS number range,
|
|
||||||
// and this is way more efficient in a tight loop.
|
|
||||||
|
|
||||||
// Another note: While this greedy algorithm possibly works for all major denominations,
|
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
|
||||||
// it still requires a fallback for cases where it might not provide any solution.
|
return [
|
||||||
// Example: Denominations: [3, 5, 10] | User inputs 4 times the [3] button, resulting in a 12 fiat tx
|
{
|
||||||
// This algorithm resolves for 1 x [10], and can't resolve the remainder of 2
|
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
|
||||||
|
denomination: cassettes[0].denomination
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provisioned: shouldFlip ? cassettes[1].count - c1 : c1,
|
||||||
|
denomination: cassettes[1].denomination
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provisioned: shouldFlip ? cassettes[2].count - c2 : c2,
|
||||||
|
denomination: cassettes[2].denomination
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provisioned: shouldFlip ? cassettes[3].count - c3 : c3,
|
||||||
|
denomination: cassettes[3].denomination
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
return _.size(cassettes) > 2 ? makeChangeDynamic(cassettes, amount) : makeChangeDuo(cassettes, amount)
|
function mergeCassettes(cassettes) {
|
||||||
|
const map = {}
|
||||||
|
|
||||||
|
_.forEach(it => {
|
||||||
|
if (!map[it.denomination]) {
|
||||||
|
map[it.denomination] = 0
|
||||||
|
}
|
||||||
|
map[it.denomination] += it.count
|
||||||
|
}, cassettes)
|
||||||
|
|
||||||
|
return _.map(it => ({ denomination: it, count: map[it] }), _.keys(map))
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmergeCassettes(cassettes, output) {
|
||||||
|
const map = {}
|
||||||
|
|
||||||
|
_.forEach(it => {
|
||||||
|
if (!map[it.denomination]) {
|
||||||
|
map[it.denomination] = 0
|
||||||
|
}
|
||||||
|
map[it.denomination] += it.provisioned
|
||||||
|
}, output)
|
||||||
|
|
||||||
|
const response = []
|
||||||
|
_.forEach(it => {
|
||||||
|
const value = {
|
||||||
|
denomination: it.denomination,
|
||||||
|
id: uuid.v4()
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountNeeded = map[it.denomination]
|
||||||
|
if (!amountNeeded) {
|
||||||
|
return response.push({ provisioned: 0, ...value })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountNeeded < it.count) {
|
||||||
|
map[it.denomination] = 0
|
||||||
|
return response.push({ provisioned: amountNeeded, ...value })
|
||||||
|
}
|
||||||
|
|
||||||
|
map[it.denomination] -= it.count
|
||||||
|
return response.push({ provisioned: it.count, ...value })
|
||||||
|
}, cassettes)
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeChangeDuo(cassettes, amount) {
|
function makeChangeDuo(cassettes, amount) {
|
||||||
|
|
@ -27,168 +79,115 @@ function makeChangeDuo (cassettes, amount) {
|
||||||
const largeDenom = large.denomination
|
const largeDenom = large.denomination
|
||||||
const smallDenom = small.denomination
|
const smallDenom = small.denomination
|
||||||
const largeBills = Math.min(large.count, Math.floor(amount / largeDenom))
|
const largeBills = Math.min(large.count, Math.floor(amount / largeDenom))
|
||||||
|
|
||||||
const amountNum = amount.toNumber()
|
const amountNum = amount.toNumber()
|
||||||
|
|
||||||
for (let i = largeBills; i >= 0; i--) {
|
for (let i = largeBills; i >= 0; i--) {
|
||||||
const remainder = amountNum - largeDenom * i
|
const remainder = amountNum - largeDenom * i
|
||||||
|
|
||||||
if (remainder % smallDenom !== 0) continue
|
if (remainder % smallDenom !== 0) continue
|
||||||
|
|
||||||
const smallCount = remainder / smallDenom
|
const smallCount = remainder / smallDenom
|
||||||
if (smallCount > small.count) continue
|
if (smallCount > small.count) continue
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{provisioned: smallCount, denomination: small.denomination, id: uuid.v4()},
|
{
|
||||||
|
provisioned: smallCount,
|
||||||
|
denomination: small.denomination,
|
||||||
|
id: uuid.v4()
|
||||||
|
},
|
||||||
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
|
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeChange(outCassettes, amount) {
|
||||||
|
const available = _.reduce(
|
||||||
|
(res, val) => res + val.count * val.denomination,
|
||||||
|
0,
|
||||||
|
outCassettes
|
||||||
|
)
|
||||||
|
|
||||||
|
if (available < amount) {
|
||||||
|
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(cassettes)}`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeChangeDynamic (cassettes, amount) {
|
const cassettes = mergeCassettes(outCassettes)
|
||||||
|
const result =
|
||||||
|
_.size(cassettes) >= 3
|
||||||
|
? makeChangeDynamic(cassettes, amount, available)
|
||||||
|
: makeChangeDuo(cassettes, amount)
|
||||||
|
|
||||||
|
if (!result.length) return null
|
||||||
|
return unmergeCassettes(outCassettes, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeChangeDynamicBruteForce(outCassettes, amount, available) {
|
||||||
|
const solutions = []
|
||||||
|
let x = 0
|
||||||
|
|
||||||
|
const shouldFlip = amount > _.max(_.map(it => it.denomination * it.count, outCassettes))
|
||||||
|
const amountNum = shouldFlip ? available - amount : amount
|
||||||
|
|
||||||
|
const cassettes = shouldFlip ? _.reverse(outCassettes) : outCassettes
|
||||||
|
const { denomination: denomination0, count: count0 } = cassettes[0]
|
||||||
|
const { denomination: denomination1, count: count1 } = cassettes[1]
|
||||||
|
const { denomination: denomination2, count: count2 } = cassettes[2]
|
||||||
|
const { denomination: denomination3, count: count3 } = cassettes[3]
|
||||||
|
|
||||||
|
const startTime = new Date().getTime()
|
||||||
|
|
||||||
|
loop1: for (let i = 0; i <= count0; i++) {
|
||||||
|
const firstSum = i * denomination0
|
||||||
|
|
||||||
|
for (let j = 0; j <= count1; j++) {
|
||||||
|
const secondSum = firstSum + j * denomination1
|
||||||
|
if (secondSum > amountNum) break
|
||||||
|
|
||||||
|
if (secondSum === amountNum) {
|
||||||
|
solutions.push(newSolution(cassettes, i, j, 0, 0, shouldFlip))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let k = 0; k <= count2; k++) {
|
||||||
|
const thirdSum = secondSum + k * denomination2
|
||||||
|
if (thirdSum > amountNum) break
|
||||||
|
|
||||||
|
if (denomination2 === 0) break
|
||||||
|
|
||||||
|
if (thirdSum === amountNum) {
|
||||||
|
solutions.push(newSolution(cassettes, i, j, k, 0, shouldFlip))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let l = 0; l <= count3; l++) {
|
||||||
|
if ((x > MAX_AMOUNT_OF_SOLUTIONS && solutions.length >= 1) || x > MAX_BRUTEFORCE_ITERATIONS) break loop1
|
||||||
|
x++
|
||||||
|
const fourthSum = thirdSum + l * denomination3
|
||||||
|
if (fourthSum > amountNum) break
|
||||||
|
|
||||||
|
if (denomination3 === 0) break
|
||||||
|
|
||||||
|
if (fourthSum === amountNum) {
|
||||||
|
solutions.push(newSolution(cassettes, i, j, k, l, shouldFlip))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTime = new Date().getTime()
|
||||||
|
|
||||||
|
console.log(`Exiting bruteforce after ${x} tries. Took ${endTime - startTime} ms`)
|
||||||
|
return solutions
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeChangeDynamic(cassettes, amount, available) {
|
||||||
while (_.size(cassettes) < 4) {
|
while (_.size(cassettes) < 4) {
|
||||||
cassettes.push({ denomination: 0, count: 0 })
|
cassettes.push({ denomination: 0, count: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const solutions = []
|
|
||||||
const amountNum = amount.toNumber()
|
const amountNum = amount.toNumber()
|
||||||
for (let i = 0; i * cassettes[0].denomination <= amountNum; i++) {
|
|
||||||
for (
|
const solutions = makeChangeDynamicBruteForce(cassettes, amountNum, available)
|
||||||
let j = 0;
|
|
||||||
i * cassettes[0].denomination + j * cassettes[1].denomination <=
|
|
||||||
amountNum;
|
|
||||||
j++
|
|
||||||
) {
|
|
||||||
if (cassettes[1].denomination === 0) break
|
|
||||||
if (
|
|
||||||
i * cassettes[0].denomination + j * cassettes[1].denomination ===
|
|
||||||
amountNum &&
|
|
||||||
i <= cassettes[0].count &&
|
|
||||||
j <= cassettes[1].count &&
|
|
||||||
i >= 0 &&
|
|
||||||
j >= 0
|
|
||||||
) {
|
|
||||||
solutions.push([
|
|
||||||
{
|
|
||||||
provisioned: i,
|
|
||||||
denomination: cassettes[0].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: j,
|
|
||||||
denomination: cassettes[1].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: 0,
|
|
||||||
denomination: cassettes[2].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: 0,
|
|
||||||
denomination: cassettes[3].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
let k = 0;
|
|
||||||
i * cassettes[0].denomination +
|
|
||||||
j * cassettes[1].denomination +
|
|
||||||
k * cassettes[2].denomination <=
|
|
||||||
amountNum;
|
|
||||||
k++
|
|
||||||
) {
|
|
||||||
if (cassettes[2].denomination === 0) break
|
|
||||||
if (
|
|
||||||
i * cassettes[0].denomination +
|
|
||||||
j * cassettes[1].denomination +
|
|
||||||
k * cassettes[2].denomination ===
|
|
||||||
amountNum &&
|
|
||||||
i <= cassettes[0].count &&
|
|
||||||
j <= cassettes[1].count &&
|
|
||||||
k <= cassettes[2].count &&
|
|
||||||
i >= 0 &&
|
|
||||||
j >= 0 &&
|
|
||||||
k >= 0
|
|
||||||
) {
|
|
||||||
solutions.push([
|
|
||||||
{
|
|
||||||
provisioned: i,
|
|
||||||
denomination: cassettes[0].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: j,
|
|
||||||
denomination: cassettes[1].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: k,
|
|
||||||
denomination: cassettes[2].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: 0,
|
|
||||||
denomination: cassettes[3].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
let l = 0;
|
|
||||||
i * cassettes[0].denomination +
|
|
||||||
j * cassettes[1].denomination +
|
|
||||||
k * cassettes[2].denomination +
|
|
||||||
l * cassettes[3].denomination <=
|
|
||||||
amountNum;
|
|
||||||
l++
|
|
||||||
) {
|
|
||||||
if (cassettes[3].denomination === 0) break
|
|
||||||
if (
|
|
||||||
i * cassettes[0].denomination +
|
|
||||||
j * cassettes[1].denomination +
|
|
||||||
k * cassettes[2].denomination +
|
|
||||||
l * cassettes[3].denomination ===
|
|
||||||
amountNum &&
|
|
||||||
i <= cassettes[0].count &&
|
|
||||||
j <= cassettes[1].count &&
|
|
||||||
k <= cassettes[2].count &&
|
|
||||||
l <= cassettes[3].count &&
|
|
||||||
i >= 0 &&
|
|
||||||
j >= 0 &&
|
|
||||||
k >= 0 &&
|
|
||||||
l >= 0
|
|
||||||
) {
|
|
||||||
solutions.push([
|
|
||||||
{
|
|
||||||
provisioned: i,
|
|
||||||
denomination: cassettes[0].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: j,
|
|
||||||
denomination: cassettes[1].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: k,
|
|
||||||
denomination: cassettes[2].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provisioned: l,
|
|
||||||
denomination: cassettes[3].denomination,
|
|
||||||
id: uuid.v4()
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedSolutions = _.sortBy(it => {
|
const sortedSolutions = _.sortBy(it => {
|
||||||
const arr = []
|
const arr = []
|
||||||
|
|
@ -206,5 +205,20 @@ function makeChangeDynamic (cassettes, amount) {
|
||||||
_.head(sortedSolutions)
|
_.head(sortedSolutions)
|
||||||
)
|
)
|
||||||
|
|
||||||
return cleanSolution
|
const response = cleanSolution
|
||||||
|
|
||||||
|
// Final sanity check
|
||||||
|
let total = 0
|
||||||
|
_.forEach(it => {
|
||||||
|
total += it.provisioned * it.denomination
|
||||||
|
}, response)
|
||||||
|
|
||||||
|
if (total === amountNum) return response
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Failed to find a solution for ${amountNum} with cassettes ${JSON.stringify(cassettes)}`
|
||||||
|
)
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = { makeChange }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue