Skip to content

Instantly share code, notes, and snippets.

@odykyi
Last active April 12, 2021 06:13
Show Gist options
  • Save odykyi/73371363f9d01103e28d2f488b66b192 to your computer and use it in GitHub Desktop.
Save odykyi/73371363f9d01103e28d2f488b66b192 to your computer and use it in GitHub Desktop.
const hashCode = str => str.split('').reduce((prevHash, currVal) =>
(((prevHash << 5) - prevHash) + currVal.charCodeAt(0)) | 0, 0);
const sortByTime = (trx, nextTrx) => new Date(trx.time) - new Date(nextTrx.time);
const getMinutesFromDates = (startDate, endDate) => {
const difference = new Date(endDate).getTime() - new Date(startDate).getTime();
return (difference / 60000);
};
const findDuplicateTransactions = (transactions = []) => {
let map = new Map();
transactions.forEach((item) => {
const hash = hashCode(item.sourceAccount + item.targetAccount + item.category + item.amount);
if (map.has(hash)) {
map.get(hash).push(item);
} else {
map.set(hash, [item]);
}
});
map = new Map([...map.entries()].sort((a, b) => new Date(a[1][0].time) - new Date(b[1][0].time)));
let result = [...map.values()];
result = result.map((arrOfTrx) => {
arrOfTrx.sort(sortByTime);
const filterByTime = (trx, index) => {
let diffMax;
let diffMin;
if (arrOfTrx[index + 1]) {
diffMax = getMinutesFromDates(trx.time, arrOfTrx[index + 1].time);
}
if (arrOfTrx[index - 1]) {
diffMin = getMinutesFromDates(arrOfTrx[index - 1].time, trx.time);
}
if ((diffMin > 0 && diffMin < 1) || (diffMax > 0 && diffMax < 1)) {
return true;
}
return false;
};
arrOfTrx = arrOfTrx.filter(filterByTime);
return arrOfTrx;
}).filter((arrOfTrx) => !arrOfTrx.length < 1);
return result;
};

Sometimes when a customer is charged, there is a duplicate transaction created. We need to find those transactions so that they can be dealt with. Everything about the transaction should be identical, except the transaction id and the time at which it occurred, as there can be up to a minute delay.

findDuplicateTransactions(transactions)

Find all transactions that have the same sourceAccount, targetAccount, category, amount, and the time difference between each consecutive transaction is less than 1 minute.

Input You can assume that all parameters will always be present and valid. However, the incoming transactions are not guaranteed to be in any particular order.

list of transactions (Transaction[]) Output list of all the duplicate transaction groups, ordered by time ascending (Transaction[][]) The groups should be sorted in ascending order of the first transaction in the group. Example Input transactions:

[ { id: 3, sourceAccount: 'A', targetAccount: 'B', amount: 100, category: 'eating_out', time: '2018-03-02T10:34:30.000Z' },

{ id: 1, sourceAccount: 'A', targetAccount: 'B', amount: 100, category: 'eating_out', time: '2018-03-02T10:33:00.000Z' },

{ id: 6, sourceAccount: 'A', targetAccount: 'C', amount: 250, category: 'other', time: '2018-03-02T10:33:05.000Z' },

{ id: 4, sourceAccount: 'A', targetAccount: 'B', amount: 100, category: 'eating_out', time: '2018-03-02T10:36:00.000Z' },

{ id: 2, sourceAccount: 'A', targetAccount: 'B', amount: 100, category: 'eating_out', time: '2018-03-02T10:33:50.000Z' },

{ id: 5, sourceAccount: 'A', targetAccount: 'C', amount: 250, category: 'other', time: '2018-03-02T10:33:00.000Z' } ];

Expected output:

[

[

{
  id: 1,
  sourceAccount: "A",
  targetAccount: "B",
  amount: 100,
  category: "eating_out",
  time: "2018-03-02T10:33:00.000Z"
},

{
  id: 2,
  sourceAccount: "A",
  targetAccount: "B",
  amount: 100,
  category: "eating_out",
  time: "2018-03-02T10:33:50.000Z"
},

{
  id: 3,
  sourceAccount: "A",
  targetAccount: "B",
  amount: 100,
  category: "eating_out",
  time: "2018-03-02T10:34:30.000Z"
}

],

[

{

  id: 5,
  sourceAccount: "A",
  targetAccount: "C",
  amount: 250,
  category: "other",
  time: "2018-03-02T10:33:00.000Z"
},

{
  id: 6,
  sourceAccount: "A",
  targetAccount: "C",
  amount: 250,
  category: "other",
  time: "2018-03-02T10:33:05.000Z"
}

]

];

let assert = require("chai").assert;
describe("findDuplicateTransactions()", function() {
it("returns empty array if there are no transactions", function() {
assert.deepEqual(findDuplicateTransactions([]), []);
});
it("returns empty array if there are no transactions", function() {
assert.deepEqual(findDuplicateTransactions([
{
id: 3,
sourceAccount: 'A',
targetAccount: 'B',
amount: 100,
category: 'eating_out',
time: '2018-03-02T10:34:30.000Z'
},
{
id: 1,
sourceAccount: 'A',
targetAccount: 'B',
amount: 100,
category: 'eating_out',
time: '2018-03-02T10:33:00.000Z'
},
{
id: 6,
sourceAccount: 'A',
targetAccount: 'C',
amount: 250,
category: 'other',
time: '2018-03-02T10:33:05.000Z'
},
{
id: 4,
sourceAccount: 'A',
targetAccount: 'B',
amount: 100,
category: 'eating_out',
time: '2018-03-02T10:36:00.000Z'
},
{
id: 2,
sourceAccount: 'A',
targetAccount: 'B',
amount: 100,
category: 'eating_out',
time: '2018-03-02T10:33:50.000Z'
},
{
id: 5,
sourceAccount: 'A',
targetAccount: 'C',
amount: 250,
category: 'other',
time: '2018-03-02T10:33:00.000Z'
}
]), [ [ { id: 5,
sourceAccount: 'A',
targetAccount: 'C',
amount: 250,
category: 'other',
time: '2018-03-02T10:33:00.000Z' },
{ id: 6,
sourceAccount: 'A',
targetAccount: 'C',
amount: 250,
category: 'other',
time: '2018-03-02T10:33:05.000Z' } ],
[ { id: 1,
sourceAccount: 'A',
targetAccount: 'B',
amount: 100,
category: 'eating_out',
time: '2018-03-02T10:33:00.000Z' },
{ id: 2,
sourceAccount: 'A',
targetAccount: 'B',
amount: 100,
category: 'eating_out',
time: '2018-03-02T10:33:50.000Z' },
{ id: 3,
sourceAccount: 'A',
targetAccount: 'B',
amount: 100,
category: 'eating_out',
time: '2018-03-02T10:34:30.000Z' } ] ]);
});
it("returns ordered and grouped array ", function() {
const inp = [{
id: 201,
sourceAccount: 'company_x',
targetAccount: 'my_account',
amount: 10000,
category: 'pension_benefits',
time: '2018-02-25T08:00:00.000Z',
},
{
id: 39,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -70,
category: 'eating_out',
time: '2018-05-15T09:12:20.000Z',
},
{
id: 23,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -70,
category: 'eating_out',
time: '2018-04-15T09:12:20.000Z',
},
{
id: 6,
sourceAccount: 'my_account',
targetAccount: 'internet_shop',
amount: -250,
category: 'other',
time: '2018-03-01T22:16:40.000Z',
},
{
id: 27,
sourceAccount: 'company_x',
targetAccount: 'my_account',
amount: 10000,
category: 'salary',
time: '2018-04-25T08:00:00.000Z',
},
{
id: 1,
sourceAccount: 'company_x',
targetAccount: 'my_account',
amount: 10000,
category: 'salary',
time: '2018-02-25T08:00:00.000Z',
},
{
id: 19,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-04-07T09:54:21.000Z',
},
{
id: 14,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-04-01T10:24:40.000Z',
},
{
id: 12,
sourceAccount: 'my_account',
targetAccount: 'bowling_place',
amount: -600,
category: 'other',
time: '2018-03-05T21:12:10.000Z',
},
{
id: 41,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -850,
category: 'groceries',
time: '2018-05-20T18:51:31.000Z',
},
{
id: 102,
sourceAccount: 'my_account',
targetAccount: 'internet_shop',
amount: -250,
category: 'other',
time: '2018-03-01T22:16:50.000Z',
},
{
id: 11,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -1540,
category: 'groceries',
time: '2018-03-05T16:24:31.000Z',
},
{
id: 13,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-04-01T10:24:00.000Z',
},
{
id: 25,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -850,
category: 'groceries',
time: '2018-04-20T18:51:31.000Z',
},
{
id: 33,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:57:05.000Z',
},
{
id: 36,
sourceAccount: 'my_account',
targetAccount: 'internet_shop',
amount: -1650,
category: 'other',
time: '2018-05-08T21:36:41.000Z',
},
{
id: 29,
sourceAccount: 'my_account',
targetAccount: 'cinema',
amount: -580,
category: 'other',
time: '2018-05-05T20:01:18.000Z',
},
{
id: 17,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -1870,
category: 'groceries',
time: '2018-04-05T10:24:30.000Z',
},
{
id: 7,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -160,
category: 'groceries',
time: '2018-03-02T13:14:00.000Z',
},
{
id: 31,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:55:10.000Z',
},
{
id: 24,
sourceAccount: 'my_account',
targetAccount: 'fitness_club',
amount: -610,
category: 'other',
time: '2018-04-22T11:54:10.000Z',
},
{
id: 5,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-03-02T09:25:20.000Z',
},
{
id: 4,
sourceAccount: 'my_account',
targetAccount: 'cinema',
amount: -330,
category: 'other',
time: '2018-03-01T20:10:15.000Z',
},
{
id: 101,
sourceAccount: 'company_x',
targetAccount: 'my_account',
amount: 240,
category: 'salary',
time: '2018-02-25T08:00:30.000Z',
},
{
id: 28,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -1870,
category: 'groceries',
time: '2018-05-05T10:24:30.000Z',
},
{
id: 15,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-04-01T10:25:10.000Z',
},
{
id: 10,
sourceAccount: 'my_account',
targetAccount: 'fitness_club',
amount: -560,
category: 'other',
time: '2018-03-04T12:54:10.000Z',
},
{
id: 35,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:58:06.000Z',
},
{
id: 38,
sourceAccount: 'my_account',
targetAccount: 'restaurant',
amount: -970,
category: 'eating_out',
time: '2018-05-17T19:52:46.000Z',
},
{
id: 3,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -1000,
category: 'groceries',
time: '2018-03-01T17:28:32.000Z',
},
{
id: 26,
sourceAccount: 'my_account',
targetAccount: 'cinema',
amount: -450,
category: 'other',
time: '2018-04-23T19:13:10.000Z',
},
{
id: 32,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:56:09.000Z',
},
{
id: 21,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -1690,
category: 'groceries',
time: '2018-04-10T18:14:10.000Z',
},
{
id: 30,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:54:21.000Z',
},
{
id: 20,
sourceAccount: 'my_account',
targetAccount: 'internet_shop',
amount: -1650,
category: 'other',
time: '2018-04-08T21:36:41.000Z',
},
{
id: 16,
sourceAccount: 'company_x',
targetAccount: 'my_account',
amount: 10000,
category: 'salary',
time: '2018-03-25T08:10:00.000Z',
},
{
id: 2,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-03-01T12:34:00.000Z',
},
{
id: 9,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-03-04T07:14:20.000Z',
},
{
id: 37,
sourceAccount: 'my_account',
targetAccount: 'supermarket',
amount: -1690,
category: 'groceries',
time: '2018-05-10T18:14:10.000Z',
},
{
id: 8,
sourceAccount: 'my_account',
targetAccount: 'restaurant',
amount: -670,
category: 'eating_out',
time: '2018-03-02T18:54:45.000Z',
},
{
id: 22,
sourceAccount: 'my_account',
targetAccount: 'restaurant',
amount: -970,
category: 'eating_out',
time: '2018-04-17T19:52:46.000Z',
},
{
id: 18,
sourceAccount: 'my_account',
targetAccount: 'cinema',
amount: -580,
category: 'other',
time: '2018-04-05T20:01:18.000Z',
},
{
id: 40,
sourceAccount: 'my_account',
targetAccount: 'fitness_club',
amount: -610,
category: 'other',
time: '2018-05-22T11:54:10.000Z',
},
{
id: 42,
sourceAccount: 'my_account',
targetAccount: 'cinema',
amount: -450,
category: 'other',
time: '2018-05-23T19:13:10.000Z',
}];
const out = [[{
id: 6,
sourceAccount: 'my_account',
targetAccount: 'internet_shop',
amount: -250,
category: 'other',
time: '2018-03-01T22:16:40.000Z',
},
{
id: 102,
sourceAccount: 'my_account',
targetAccount: 'internet_shop',
amount: -250,
category: 'other',
time: '2018-03-01T22:16:50.000Z',
}],
[{
id: 13,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-04-01T10:24:00.000Z',
},
{
id: 14,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-04-01T10:24:40.000Z',
},
{
id: 15,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -50,
category: 'eating_out',
time: '2018-04-01T10:25:10.000Z',
}],
[{
id: 30,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:54:21.000Z',
},
{
id: 31,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:55:10.000Z',
},
{
id: 32,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:56:09.000Z',
},
{
id: 33,
sourceAccount: 'my_account',
targetAccount: 'coffee_shop',
amount: -90,
category: 'eating_out',
time: '2018-05-07T09:57:05.000Z',
}]];
assert.deepEqual(findDuplicateTransactions(inp), out);
});
// add your tests here
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment