Let's say I have a sorted array by name and then date: (The format of the date is dd-mm-yy)
[{name: "A", date: "1/1/2022"},
{name: "A", date: "2/1/2022"},
{name: "A", date: "13/1/2022"},
{name: "B", date: "1/3/2022"},
{name: "B", date: "3/7/2022"},
{name: "B", date: "6/12/2022"},
{name: "C", date: "11/1/2022"},
{name: "C", date: "1/5/2022"},
{name: "C", date: "1/6/2022"},
{name: "C", date: "1/7/2022"}]
How can I count the number of the name that appears at least 3 times within 3 months?
For example:
A appears in 1/1/22, 2/1/22, 13/1/22, so it is counted.
B isn't counted because it did not appear 3 times or above within 3 months.
C is counted. Although its first appearance and second appearance is four months long, its 2nd, 3rd and 4th appearances were within 3 months, so it is counted.
Once the name is counted, the loop can skip to a new name immediately.
So, the return number is 2.
CodePudding user response:
You could get epoch time and check delta for three following items.
const
getISO = d => d.replace(/^(\d )\/(\d )\/(\d )$/, (_, d, m, y) => `${y}-${m.padStart(2, 0)}-${d.padStart(2, 0)}`),
day = 1000 * 60 * 60 * 24,
data = [{ name: "A", date: "1/1/2022" }, { name: "A", date: "2/1/2022" }, { name: "A", date: "13/1/2022" }, { name: "B", date: "1/3/2022" }, { name: "B", date: "3/7/2022" }, { name: "B", date: "6/12/2022" }, { name: "C", date: "11/1/2022" }, { name: "C", date: "1/5/2022" }, { name: "C", date: "1/6/2022" }, { name: "C", date: "1/7/2022" }],
temp = data.reduce((r, o) => {
(r[o.name] ??= []).push(new Date(getISO(o.date)).getTime());
return r;
}, {}),
result = Object
.keys(temp)
.filter(k => {
for (let i = 0; i < temp[k].length - 2; i ) {
if ((temp[k][i 2] - temp[k][i]) / day <= 90) return true;
}
});
console.log(result);
CodePudding user response:
In the old fashion style
var a = [{name: "A", date: "1/1/2022"},
{name: "A", date: "2/1/2022"},
{name: "A", date: "13/1/2022"},
{name: "B", date: "1/3/2022"},
{name: "B", date: "3/7/2022"},
{name: "B", date: "6/12/2022"},
{name: "C", date: "11/1/2022"},
{name: "C", date: "1/5/2022"},
{name: "C", date: "1/6/2022"},
{name: "C", date: "1/7/2022"}]
a = a.map(function(e){
e.date = new Date(e.date.substr(0, e.date.length - 5).split("/").reverse().join("/") "/" e.date.substr(-4));
return e;
}).sort(function(a, b){
return a.date - b.date;
});
var sliced = [];
for(var i = 0; i < a.length; i )
{
var slice = a.slice(i).filter(function(e){
var d = new Date(e.date);
d.setMonth(d.getMonth() - 3);
if(d.getTime() > a[i].date.getTime())
{
return false;
}
else
{
return true;
}
}).map(function(e){
return e.name;
}).reduce(function(acc, e){
acc[e] = (acc[e] || 0) 1;
return acc;
}, {});
sliced.push(slice);
// debug
//console.log(slice);
}
var result = sliced.flat().reduce(function(acc, e){
Object.keys(e).filter(function(k){
return e[k] >= 3;
}).forEach(function(k){
acc[k] = 1;
});
return acc;
}, {});
console.log(result);
console.log(Object.keys(result).length);
CodePudding user response:
You could start by grouping the values by name, so we have a map of the names and each entry.
We then iterate over this map, counting the number of dates within 3 months of each other, using a custom function countWithinNMonths()
, counting the number of subsequent months within the threshold value.
If any of these exceeds the threshold count, in this case 3, we use Array.filter to return the name in question.
This will give use a list of matching names, it's then easy to get the count.
const input = [{name: "A", date: "1/1/2022"}, {name: "A", date: "2/1/2022"}, {name: "A", date: "13/1/2022"}, {name: "B", date: "1/3/2022"}, {name: "B", date: "3/7/2022"}, {name: "B", date: "6/12/2022"}, {name: "C", date: "11/1/2022"}, {name: "C", date: "1/5/2022"}, {name: "C", date: "1/6/2022"}, {name: "C", date: "1/7/2022"}];
function groupBy(arr, field) {
return arr.reduce((acc, el) => {
acc[el[field]] = acc[el[field]] || [];
acc[el[field]].push(el);
return acc;
}, {})
}
function parseDMY(dmy) {
const [day, month, year] = dmy.split(/[\/-]/);
return new Date(year, month - 1, day)
}
function countWithinNMonths(dates, months) {
return dates.sort((a,b) => a - b).map((d1) => dates.filter((d2) => (d1 <= d2) && (dateDiffMonths(d1, d2)) <= months).length);
}
function dateDiffMonths(d1, d2) {
return (d2.getFullYear() * 12 d2.getMonth() d2.getDate() / 30) - (d1.getFullYear() * 12 d1.getMonth() d1.getDate() / 30);
}
const monthThreshold = 3;
const countThreshold = 3;
const groupedByName = groupBy(input, 'name');
const matchingNames = Object.keys(groupedByName).filter(name => {
const dates = groupedByName[name].map(v => parseDMY(v.date));
const counts = countWithinNMonths(dates, monthThreshold);
const maxCount = Math.max(...counts)
return maxCount >= countThreshold;
});
console.log('Matching names:', matchingNames)
console.log('Match count:', matchingNames.length)
A slightly different approach, without grouping first is below.
We create a list of all items, and the subsequent items within three months, then filter to create our final list.
const input = [{name: "A", date: "1/1/2022"}, {name: "A", date: "2/1/2022"}, {name: "A", date: "13/1/2022"}, {name: "B", date: "1/3/2022"}, {name: "B", date: "3/7/2022"}, {name: "B", date: "6/12/2022"}, {name: "C", date: "11/1/2022"}, {name: "C", date: "1/5/2022"}, {name: "C", date: "1/6/2022"}, {name: "C", date: "1/7/2022"}];
function parseDMY(dmy) {
const [day, month, year] = dmy.split(/[\/-]/);
return new Date(year, month - 1, day)
}
function getMatches(items, months, minCount) {
return items.filter(item1 => items.filter(item2 => {
const diffMonths = dateDiffMonths(parseDMY(item1.date), parseDMY(item2.date));
return item1.name === item2.name &&
diffMonths >= 0 &&
diffMonths <= months;
}).length >= minCount
);
}
function dateDiffMonths(d1, d2) {
return (d2.getFullYear() * 12 d2.getMonth() d2.getDate() / 30) - (d1.getFullYear() * 12 d1.getMonth() d1.getDate() / 30);
}
const monthThreshold = 3;
const countThreshold = 3;
const matches = getMatches(input, monthThreshold, countThreshold);
const names = [...new Set(matches.map(x => x.name))];
console.log('Matching names:', names);
console.log('Matching name count:', names.length);