feat: add bill math cassette-agnostic solution
fix: multiple generic fixes related with the recyclers fix: slight UI data changes
This commit is contained in:
parent
2d010fc359
commit
f3ab63766e
15 changed files with 173 additions and 353 deletions
247
lib/bill-math.js
247
lib/bill-math.js
|
|
@ -1,225 +1,46 @@
|
|||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
const sumService = require('@haensl/subset-sum')
|
||||
|
||||
const MAX_AMOUNT_OF_SOLUTIONS = 10000
|
||||
const MAX_BRUTEFORCE_ITERATIONS = 10000000
|
||||
|
||||
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
|
||||
return [
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
|
||||
denomination: cassettes[0].denomination
|
||||
const getSolution = (units, amount) => {
|
||||
const billList = _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push(..._.times(_.constant(value.denomination), value.count))
|
||||
return acc
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[1].count - c1 : c1,
|
||||
denomination: cassettes[1].denomination
|
||||
[],
|
||||
units
|
||||
)
|
||||
|
||||
const solver = sumService.subsetSum(billList, amount.toNumber())
|
||||
const solution = _.countBy(Math.floor, solver.next().value)
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
acc.push({ denomination: _.toNumber(value), provisioned: solution[value] })
|
||||
return acc
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[2].count - c2 : c2,
|
||||
denomination: cassettes[2].denomination
|
||||
},
|
||||
{
|
||||
provisioned: shouldFlip ? cassettes[3].count - c3 : c3,
|
||||
denomination: cassettes[3].denomination
|
||||
}
|
||||
]
|
||||
[],
|
||||
_.keys(solution)
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
// Initialize empty cassettes in case of undefined, due to same denomination across all cassettes results in a single merged cassette
|
||||
const small = cassettes[0] ?? { denomination: 0, count: 0 }
|
||||
const large = cassettes[1] ?? { denomination: 0, count: 0 }
|
||||
|
||||
const largeDenom = large.denomination
|
||||
const smallDenom = small.denomination
|
||||
const largeBills = Math.min(large.count, Math.floor(amount / largeDenom))
|
||||
const amountNum = amount.toNumber()
|
||||
|
||||
for (let i = largeBills; i >= 0; i--) {
|
||||
const remainder = amountNum - largeDenom * i
|
||||
|
||||
if (remainder % smallDenom !== 0) continue
|
||||
const smallCount = remainder / smallDenom
|
||||
if (smallCount > small.count) continue
|
||||
return [
|
||||
{
|
||||
provisioned: smallCount,
|
||||
denomination: small.denomination,
|
||||
id: uuid.v4()
|
||||
},
|
||||
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
const solutionToOriginalUnits = (solution, units) => {
|
||||
const billsLeft = _.clone(_.fromPairs(_.map(it => [it.denomination, it.provisioned])(solution)))
|
||||
return _.reduce(
|
||||
(acc, value) => {
|
||||
const unit = units[value]
|
||||
const billsToAssign = _.clamp(0, unit.count)(_.isNaN(billsLeft[unit.denomination]) || _.isNil(billsLeft[unit.denomination]) ? 0 : billsLeft[unit.denomination])
|
||||
acc.push({ name: unit.name, denomination: unit.denomination, provisioned: billsToAssign })
|
||||
billsLeft[unit.denomination] -= billsToAssign
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
_.range(0, _.size(units))
|
||||
)
|
||||
}
|
||||
|
||||
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(outCassettes)}`)
|
||||
return null
|
||||
}
|
||||
|
||||
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) {
|
||||
cassettes.push({ denomination: 0, count: 0 })
|
||||
}
|
||||
|
||||
const amountNum = amount.toNumber()
|
||||
|
||||
const solutions = makeChangeDynamicBruteForce(cassettes, amountNum, available)
|
||||
|
||||
const sortedSolutions = _.sortBy(it => {
|
||||
const arr = []
|
||||
|
||||
for (let la = 0; la < 4; la++) {
|
||||
arr.push(cassettes[la].count - it[la].provisioned)
|
||||
}
|
||||
|
||||
if (arr.length < 2) return Infinity
|
||||
return _.max(arr) - _.min(arr)
|
||||
}, solutions)
|
||||
|
||||
const cleanSolution = _.filter(
|
||||
it => it.denomination > 0,
|
||||
_.head(sortedSolutions)
|
||||
)
|
||||
|
||||
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 []
|
||||
const solution = getSolution(outCassettes, amount)
|
||||
return solutionToOriginalUnits(solution, outCassettes)
|
||||
}
|
||||
|
||||
module.exports = { makeChange }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue