Skip to content

Instantly share code, notes, and snippets.

@jenyayel
Created August 3, 2021 07:46
Show Gist options
  • Save jenyayel/599c1ce4e528e5f878b4e992b4e24555 to your computer and use it in GitHub Desktop.
Save jenyayel/599c1ce4e528e5f878b4e992b4e24555 to your computer and use it in GitHub Desktop.
Coding challenge
/**
* 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
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