Desafio: Retirada de dinheiro em Javascript como caixa eletrônico

Esses dias fiquei pensando como funcionava a funcao que faz a retirada de dinheiro de um caixa eletronico. Parei para tentar fazer essa funcao e confesso que foi desafiador, fiz varios loops e coisas malukas até chegar a um resultado que gostaria de compartilhar com vocês

Basicamente pensei em função em javascript que faça saque com base no saldo e um array de notas disponivel, exemplo, preciso sacar 5 reais, e tem notas de 5 notas de 10, 4 notas de 20, 3 de 50, 5 de 100, logo nao teria o valor de 5 reais, e se eu sacar 550 descontar das notas e sempre fazer da melhor forma possivel.

function cashWithdrawal(value, availableNotes) {
    let notes = [];
    let availableNotesOriginal = structuredClone(availableNotes);
    // array com os valores de todas as notas disponíveis
    const allNotesValues = Object.keys(availableNotes).map((note) => Number(note)).sort((a, b) => b - a);
    for (const subsetNotes of subsets(allNotesValues, 0, 2)) {
        let remainingValue = value;
        for (const noteValue of subsetNotes) {
            // quantas notas desse valor eu preciso?
            const need = Math.min(Math.floor(remainingValue / noteValue), availableNotes[noteValue]);
            for (let i = 0; i < need; i++) { // adiciona as notas
                notes.push(noteValue);
            }
            // subtrai a quantidade e atualiza o valor restante
            availableNotes[noteValue] -= need;
            remainingValue -= noteValue * need;
        }
        if (remainingValue == 0) { // se o valor zerou, é porque deu certo
            const availableNotesSum = Object.entries(availableNotes).reduce((sum, [note, quantity]) => sum + Number(note) * quantity, 0);
            return {
                notes,
                availableNotes,
                availableNotesSum
            };
        } else {
            // se não deu certo, zera o array de notas e volta para as notas disponíveis original
            notes = [];
            availableNotes = JSON.parse(JSON.stringify(availableNotesOriginal));
        }
        // se o valor não zerou, é porque não foi possível, então tenta com a próxima combinação de notas
    }
    return null; // se tentou todas as combinações e chegou aqui, é porque não é possível
}

Exemplo de uso:

const availableNotes = {10: 4, 20: 4, 50: 3, 100: 5};
const value = 580;

console.log('availableNotesSum', Object.entries(availableNotes).reduce((sum, [note, quantity]) => sum + Number(note) * quantity, 0))

const result = cashWithdrawal(value, availableNotes);
console.log(result);
// {
//   notes: [100, 100, 100, 100, 100, 50, 50, 50, 20, 20, 20, 20, 10, 10, 10, 10],
//   remainingValue: 0,
//   availableNotes: {10: 0, 20: 0, 50: 0, 100: 1},
//   availableNotesSum: 100,
// }

Agradecimentos especial ao @kht pela contribuição! Criei um gist para deixar isso salvo por lá https://gist.github.com/joaosouz4dev/d9bcce6f45f1af0aa3b337db847ec09d

Só um detalhe, teste o seu código com:

const availableNotes = {
    20: 4,
    50: 3,
    100: 5
};
const value = 60;

O seu código vai retornar notes: [50]. Ou seja, apenas uma nota de 50. Mas isso não é suficiente para dar o valor (60), pois o correto seria retornar 3 notas de 20.

E se testar com:

const availableNotes = {
    20: 2
};
const value = 60;

Vai retornar:

{
  notes: [ 20, 20, 20 ],
  availableNotes: { '20': -1 },
  availableNotesSum: -20
}

Isso porque faltou verificar se a quantidade não zerou.


Para arrumar isso, temos que modificar um pouco o código. Primeiro temos que testar com todas as notas, e se não der, testamos sem uma das notas (por exemplo, sem as notas de 100). Se não der, testamos sem as notas de 50 e assim por diante, até encontrar uma combinação que dê certo. Se todas as combinações falharem, é porque não é possível.

Ficaria assim:

// gera todas as combinações do array
function *subsets(array, offset = 0, minSize = 0) {
    while (offset < array.length) {
        let first = array[offset++];
        for (let subset of subsets(array, offset)) {
            subset.splice(0, 0, first);
            if (subset.length >= minSize)
                yield subset;
        }
    }
    if (minSize === 0)
        yield[];
}

function cashWithdrawal(value, availableNotes) {
    let notes = [];
    let availableNotesOriginal = structuredClone(availableNotes);
    // array com os valores de todas as notas disponíveis
    const allNotesValues = Object.keys(availableNotes).map((note) => Number(note)).sort((a, b) => b - a);
    for (const subsetNotes of subsets(allNotesValues, 0, 2)) {
        let remainingValue = value;
        for (const noteValue of subsetNotes) {
            while (remainingValue >= noteValue && availableNotes[noteValue] > 0) {
                notes.push(noteValue);
                remainingValue -= noteValue;
                availableNotes[noteValue]--;
            }
        }
        if (remainingValue == 0) { // se o valor zerou, é porque deu certo
            const availableNotesSum = Object.entries(availableNotes).reduce((sum, [note, quantity]) => sum + Number(note) * quantity, 0);
            return {
                notes,
                availableNotes,
                availableNotesSum
            };
        } else {
            // se não deu certo, zera o array de notas e volta para as notas disponíveis original
            notes = [];
            availableNotes = JSON.parse(JSON.stringify(availableNotesOriginal));
        }
        // se o valor não zerou, é porque não foi possível, então tenta com a próxima combinação de notas
    }
    return null; // se tentou todas as combinações e chegou aqui, é porque não é possível
}
let availableNotes = {
    20: 4,
    50: 3,
    100: 5
};
let value = 60;

console.log(cashWithdrawal(value, availableNotes));

Resultado (3 notas de 20):

{
  notes: [ 20, 20, 20 ],
  availableNotes: { '20': 1, '50': 3, '100': 5 },
  availableNotesSum: 670
}

Se eu mudar o valor para 75, a função retorna null, pois não é possível obter o valor desejado com as notas disponíveis. E se testar com availableNotes = { 20: 2 }; e value = 60;, também vai retornar null, pois também não é possível atingir o valor.


A função subsets é uma generator function, que em vez de retornar todas as combinações de uma vez, ela "retorna" apenas uma de cada vez (isso economiza memória, pois dependendo da quantidade de notas, a quantidade de combinações possíveis pode ficar muito grande).

Como eu faço um loop pelos subsets e verifico um por um (e retorno se encontrar um que deu certo), isso faz com que o código consuma menos memória, pois eu trabalho apenas com uma combinação por vez, e em seguida a descarto para ir para a próxima.

Eu também clono o availableNotes com structuredClone porque se uma combinação não der certo, preciso restaurar os valores originais. Mas vc também pode trocar por JSON.parse(JSON.stringify(availableNotes)).

Nossa, perfeita colocação. Realmente falhei mesmo assim no codigo. Vou adicionar isso e testar mais.
E outra coisa, não precisa do `while` para ficar subtraindo uma nota de cada vez. Vc pode ver quantas notas precisa, e subtrair tudo de uma vez: ```javascript function cashWithdrawal(value, availableNotes) { let notes = []; let availableNotesOriginal = structuredClone(availableNotes); // array com os valores de todas as notas disponíveis const allNotesValues = Object.keys(availableNotes).map((note) => Number(note)).sort((a, b) => b - a); for (const subsetNotes of subsets(allNotesValues, 0, 2)) { let remainingValue = value; for (const noteValue of subsetNotes) { // quantas notas desse valor eu preciso? const need = Math.min(Math.floor(remainingValue / noteValue), availableNotes[noteValue]); for (let i = 0; i < need; i++) { // adiciona as notas notes.push(noteValue); } // subtrai a quantidade e atualiza o valor restante availableNotes[noteValue] -= need; remainingValue -= noteValue * need; } if (remainingValue == 0) { // se o valor zerou, é porque deu certo const availableNotesSum = Object.entries(availableNotes).reduce((sum, [note, quantity]) => sum + Number(note) * quantity, 0); return { notes, availableNotes, availableNotesSum }; } else { // se não deu certo, zera o array de notas e volta para as notas disponíveis original notes = []; availableNotes = JSON.parse(JSON.stringify(availableNotesOriginal)); } // se o valor não zerou, é porque não foi possível, então tenta com a próxima combinação de notas } return null; // se tentou todas as combinações e chegou aqui, é porque não é possível } ``` Ou seja, basta dividir o valor restante pelo valor da nota para saber quantas eu preciso. Mas se não tiver notas suficientes, eu pego tudo que tem disponível e continuo com a próxima nota. E faço isso até acabar as notas, e vejo se o valor restante é zero, para saber se deu certo.
Fiz uma pequena correção (editei a [resposta acima](https://www.tabnews.com.br/kht/f0e07a07-ff5a-465f-8001-17715fd79bb9)), o `if (remainingValue == 0)` tem que ficar fora do `for`.