Change machine
sum()
Sum arguments or array.
1
2
3
4
> sum(1, 2, 3)
6
> sum([1, 2, 3])
6
1
2
3
4
5
6
const sum = function(seq) {
const add = (a, b) => a + b;
return Array.isArray(seq)
? seq.reduce(add)
: [...arguments].reduce(add);
};
mirror()
Mirror an object.
1
2
> mirror({'Z': 'A', 'Y': 'B'})
{ A: 'Z', B: 'Y' }
1
2
3
4
5
const mirror = obj =>
Object.keys(obj).reduce((acc, curr) => {
acc[obj[curr]] = curr;
return acc;
}, {});
Object.values()
Polyfill for ES2017 draft Object.values().
Return an array containing the values in obj
.
1
2
> Object.values({'a': 1, 'b': 2, 'c': 3})
[ 1, 2, 3 ]
1
2
3
4
5
6
7
Object.prototype.values = function(obj) {
const result = [];
for (const k of Object.keys(obj)) {
result.push(obj[k]);
}
return result;
};
change()
Return change in the fewest possible number of specified return currency units, returning any non-permitted denominations or objects along with change.
1
2
3
4
5
6
> d = {'$20': 2000, '$10': 1000, '$5': 500, '$1': 100}
> rd = {'$10': 1000, '$5': 500, '$1': 100}
> change(d, rd, '$20'))
$10 $10
> change(d, rd, '$5'))
$1 $1 $1 $1 $1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const change = (denoms, returnDenoms, inserted) => {
/* We want to be able to look up denomination faces and values
in both directions. */
const revLook = mirror(denoms);
/* When returning change, we need to evaluate denominations in
descending order. */
const valsDesc = Object.values(returnDenoms).sort((a, b) => b - a);
/* To help always make change if possible, by noting if we're not
yet working with the smallest desired return denomination value. */
const smallest = valsDesc[valsDesc.length - 1];
/* Evaluate the input. */
const input = inserted.split(' ');
const reject = input.filter(elt => !Object.keys(denoms).includes(elt));
const accept = input.filter(elt => !reject.includes(elt));
let total = !accept.length ? 0 : sum(accept.map(elt => denoms[elt]));
/* Make change. */
const change = [];
parseByVal: for (const v of valsDesc) {
while (total >= v) {
/* Always make change if possible. */
if (!change.length && total === v && total > smallest) {
continue parseByVal;
}
change.push(revLook[v]);
total -= v;
}
}
return change.concat(reject).join(' ');
};
Tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const assert = require('assert');
// Permitted input denominations.
const denoms = {
'$20': 2000, '$10': 1000, '$5': 500, '$2': 200, '$1': 100,
'50¢': 50, '25¢': 25, '10¢': 10
};
// Desired return denominations.
const returnDenoms = {
'$10': 1000, '$2': 200, '25¢': 25, '10¢': 10
};
describe('change()', function () {
it('should return unacceptable denominations or objects', function () {
assert.equal(change(denoms, returnDenoms, '$100'), '$100');
assert.equal(change(denoms, returnDenoms, 'parking ticket'), 'parking ticket');
});
it('should make change in the fewest possible number of desired return currency units', function () {
assert.equal(change(denoms, returnDenoms, "$1"), "25¢ 25¢ 25¢ 25¢");
assert.equal(change(denoms, returnDenoms, "$5"), "$2 $2 25¢ 25¢ 25¢ 25¢");
});
it('should return unacceptable denominations or objects but still make change', function () {
assert.equal(change(denoms, returnDenoms, '$1 parking ticket'), '25¢ 25¢ 25¢ 25¢ parking ticket');
});
it('should always make change if possible', function () {
assert.equal(change(denoms, returnDenoms, '50¢'), '25¢ 25¢');
assert.equal(change(denoms, returnDenoms, "$20"), "$10 $10");
});
});