Created
August 3, 2021 07:46
-
-
Save jenyayel/599c1ce4e528e5f878b4e992b4e24555 to your computer and use it in GitHub Desktop.
Coding challenge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Returns a modified array of transactions where each categorized based on similarity. | |
*/ | |
const categorizeSimilarTransactions = (transactions) => { | |
if (transactions.length < 2) { | |
return transactions; | |
} | |
// Since each transaction can be matched | |
// only to transaction that has category, | |
// we going to create a hashtable, where key is targetAccount (because | |
// similarity can be only when targetAccount are the same) to improve lookups | |
const validForMatching = transactions | |
.filter((t) => typeof t.category !== 'undefined') | |
.reduce( | |
(acc, curr) => { | |
if (!acc[curr.targetAccount]) { | |
acc[curr.targetAccount] = []; | |
} | |
acc[curr.targetAccount].push(curr); | |
return acc; | |
}, | |
{}); | |
// traverse all transactions and find a similar for each one | |
for (const tr of transactions) { | |
if (typeof (tr.category) !== 'undefined') { | |
// the transaction is already categorized | |
continue; | |
} | |
if(!validForMatching[tr.targetAccount]) { | |
// no transactions with the same targetAccount exists | |
continue; | |
} | |
// for readibility | |
const uncategorized = tr; | |
// find best matching transaction | |
const { transaction: bestMatch } = validForMatching[tr.targetAccount] | |
.reduce( | |
(best, current) => { | |
// similarity score as an absolute number, where lower number means better match | |
const score = Math.abs(current.amount - uncategorized.amount); | |
if (best.score > score && score <= 1000) { | |
// the `current` transaction is better match than the one we had so far | |
return { transaction: current, score }; | |
} | |
return best; | |
}, | |
// this is tre structure to keep track which is | |
// the best result we found so far | |
{ transaction: undefined, score: Number.MAX_SAFE_INTEGER } | |
); | |
if (bestMatch) { | |
uncategorized.category = bestMatch.category; | |
} else { | |
console.warn(`Didn't find matching transaction`, uncategorized); | |
} | |
} | |
return transactions; | |
}; | |
module.exports = categorizeSimilarTransactions |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const categorizeSimilarTransactions = require('./categorizeSimilarTransactions') | |
const genTr = (amount, targetAccount, category) => ({ | |
id: Math.random() + '', | |
sourceAccount: 'my_account', | |
targetAccount, | |
amount, | |
time: new Date().toISOString(), | |
category | |
}); | |
describe('categorizeSimilarTransactions()', () => { | |
it('returns empty array if transactions is empty', () => { | |
expect(categorizeSimilarTransactions([])).toEqual([]); | |
}); | |
it('enhances categorization when there are similar transactions', () => { | |
expect( | |
categorizeSimilarTransactions([ | |
{ | |
id: 'a001bb66-6f4c-48bf-8ae0-f73453aa8dd5', | |
sourceAccount: 'my_account', | |
targetAccount: 'coffee_shop', | |
amount: -620, | |
time: '2021-04-10T10:30:00Z', | |
}, | |
{ | |
id: 'bfd6a11a-2099-4b69-a7bb-572d8436cf73', | |
sourceAccount: 'my_account', | |
targetAccount: 'coffee_shop', | |
amount: -350, | |
category: 'eating_out', | |
time: '2021-03-12T12:34:00Z', | |
}, | |
{ | |
id: 'a8170ced-1c5f-432c-bb7d-867589a9d4b8', | |
sourceAccount: 'my_account', | |
targetAccount: 'coffee_shop', | |
amount: -1690, | |
time: '2021-04-12T08:20:00Z', | |
}, | |
]) | |
).toEqual([ | |
{ | |
id: 'a001bb66-6f4c-48bf-8ae0-f73453aa8dd5', | |
sourceAccount: 'my_account', | |
targetAccount: 'coffee_shop', | |
amount: -620, | |
category: 'eating_out', | |
time: '2021-04-10T10:30:00Z', | |
}, | |
{ | |
id: 'bfd6a11a-2099-4b69-a7bb-572d8436cf73', | |
sourceAccount: 'my_account', | |
targetAccount: 'coffee_shop', | |
amount: -350, | |
category: 'eating_out', | |
time: '2021-03-12T12:34:00Z', | |
}, | |
{ | |
id: 'a8170ced-1c5f-432c-bb7d-867589a9d4b8', | |
sourceAccount: 'my_account', | |
targetAccount: 'coffee_shop', | |
amount: -1690, | |
time: '2021-04-12T08:20:00Z', | |
}, | |
]); | |
}); | |
it('should not categorize transaction that has no similar account', () => { | |
// arrange | |
const transactions = [ | |
genTr(100, 'account 1', 'car_rental'), | |
genTr(300, 'account 2', 'car_rental'), | |
genTr(100, 'account 3', undefined) | |
]; | |
// act | |
const actual = categorizeSimilarTransactions(transactions); | |
// assert | |
expect(actual) | |
.toEqual(expect.arrayContaining([ | |
expect.objectContaining({ | |
id: transactions[2].id, | |
targetAccount: 'account 3', | |
category: undefined, | |
})])); | |
}); | |
it('should categorize transaction that has matching account', () => { | |
// arrange | |
const transactions = [ | |
genTr(150, 'account 1', 'car_rental'), | |
genTr(150, 'account 1', 'car_rental'), | |
genTr(100, 'account 2', 'education'), | |
genTr(200, 'account 2', 'education'), | |
genTr(150, 'account 2', undefined) | |
]; | |
// act | |
const actual = categorizeSimilarTransactions(transactions); | |
// assert | |
expect(actual) | |
.toEqual(expect.arrayContaining([ | |
expect.objectContaining({ | |
id: transactions[4].id, | |
targetAccount: 'account 2', | |
category: 'education', | |
})])); | |
}); | |
it('should categorize multiple transactions for different similar transactions', () => { | |
// arrange | |
const transactions = [ | |
genTr(100, 'account 1', 'car_rental'), | |
genTr(200, 'account 1', 'car_rental'), | |
genTr(100, 'account 2', 'education'), | |
genTr(200, 'account 2', 'education'), | |
genTr(150, 'account 1', undefined), | |
genTr(150, 'account 2', undefined) | |
]; | |
// act | |
const actual = categorizeSimilarTransactions(transactions); | |
// assert | |
expect(actual) | |
.toEqual(expect.arrayContaining([ | |
expect.objectContaining({ | |
id: transactions[4].id, | |
targetAccount: 'account 1', | |
category: 'car_rental', | |
}), | |
expect.objectContaining({ | |
id: transactions[5].id, | |
targetAccount: 'account 2', | |
category: 'education', | |
})])); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment