calculate sum of income with Firestore - javascript

I'mm trying to calculate sum of serviceCost from Firestore. I managed to get all the docs but it seems that the query calculated only the first cost entered of given month.
Code:
firebase
.firestore()
.collection("users")
.doc(uid)
.collection("confirmed-appointments")
.get()
.then((querySnapshot) => {
let serviceCostTotal = 0; //Will hold currentMonth Total Income.
let monthNumber = 0;
let array = [];
querySnapshot.forEach((doc) => {
monthNumber = parseInt(doc.data().month, 10);
serviceCostTotal =
serviceCostTotal + parseInt(doc.data().serviceCost, 10); //Calculate Total Month income using this formula
array[monthNumber - 1] = serviceCostTotal; //Push the income of month X to array in X place
serviceCostTotal = 0; // after pushing, initialize the sum to 0
});
For example:
I want to calculate the total serviceCost for month 10.
Firestore looks like this:
In the loop I'm trying to take the serviceCost of each doc, and push it to array[monthNumber].
The problem is: if i have 2 docs that their value is the same "month: xx" , the loop calculate only the serviceCost value of the first doc.
Meaning that if my sum needs to be 6000, it will be only 2500.
its calculating only this:
while i have this also:

You're explicitly overwriting each element. Try adding to it instead.
array[monthNumber - 1] = serviceCostTotal
// Should be
array[monthNumber - 1] = (array[monthNumber - 1] || 0) + serviceCostTotal
Edit:
As others have said this code works
let monthNumber = 0;
let array = [];
[{ month: 1, serviceCost: 15 },
{ month: 1, serviceCost: 4 },
{ month: 1, serviceCost: 9 },
{ month: 2, serviceCost: 1 },
{ month: 3, serviceCost: 100 }].forEach((doc) => {
monthNumber = parseInt(doc.month, 10);
serviceCostTotal =
serviceCostTotal + parseInt(doc.serviceCost, 10); //Calculate Total Month income using this formula
array[monthNumber - 1] = (array[monthNumber - 1] || 0) + serviceCostTotal; //Push the income of month X to array in X place
serviceCostTotal = 0; // after pushing, initialize the sum to 0
});
console.log(array);
You have to show us how you are validating this and what the actual input and outputs are.

As you said you already have the data in the array index so you can do like this
const querySnapshot = [{
month: 10,
serviceCost: 2500
}, {
month: 10,
serviceCost: 3500
}, {
month: 11,
serviceCost: 1000
}];
const array = [];
querySnapshot.forEach((doc) => {
const monthNumber = parseInt(doc.month, 10);
array[monthNumber - 1] = (array[monthNumber - 1] || 0) + parseInt(doc.serviceCost, 10);
});
console.log(array);
You basically check if whether there is something in that index of that month and add it or use the serviceCostTotal that you calculated

Related

How to calculate the sum and product of each element of multiple arrays?

Please bear with me this is difficult to explain. I will first explain how to do it successfully with only one set of data.
First, say I have an array like so yValuesMinusMean = [-5, -4, -1, 10]
I have another array like so xValuesMinusMean = [ 2.75,3.75,6.75,5.75 ]
Both of the above arrays can have numerous values. However, the length of both arrays is the same. So if the first one has 4, then the second one will definitely have 4.
I want to calculate the sum and product of the arrays. This is what I mean:
var sumOfXTimesYValues = this.calculateProductAndSum(yValuesMinusMean, xValuesMinusMean);
calculateProductAndSum(yValuesMinusMean = [], xValuesMinusMean = []) {
let total = 0;
for(let i = 0; i < yValuesMinusMean.length; i++) {
let product = (yValuesMinusMean[i] * xValuesMinusMean[i]);
total += product;
};
return total;
},
The result of this: console.log('sumOfXTimesYValues', sumOfXTimesYValues); is
17
LOGIC : (-5 * 2.75) + (-4 * 3.75) + (-1 * 6.75) + (10 * 5.25) = 17
So far, everything works. However, I want to make it so that instead of xValuesMinusMean being a single array with multiple numerical values, it will be a single array containing multiple arrays, with each array having the same number of elements as in yValuesMinusMean. Like so:
xValuesMinusMean = [ [ 2.75,3.75,6.75,5.75 ], [-2,-1,2,1]. .... ]
END GOAL: sumOfXTimesYValues = [17, 22, ...]
Logic for the second array item: (-5 * -2) + (-4 * -1) + (-1 * 2) + (10 * 1) = 22
Essentially, you're multiplying each value in each array in xValuesMinusMean with a value in yValuesMinusMean in the same order. So, -5 is the 0th item in the yValuesMinusMean array, and -2 is the 0th item in the array within xValuesMinusMean. So -5 * -2.
My next steps would be to do something like this:
xValuesMinusMean.forEach(element => {
for(let i = 0; i < xValuesMinusMean.length; i++) {
let product = (newCategoryArray[i] * xValuesMinusMean[i]);
total += product;
};
});
However, it yields the following: sumOfXTimesYValues = 352, which isn't correct. How would I be able to achieve the end goal?
END GOAL: sumOfXTimesYValues = [17, 22, ...]
Create a generic function for computing a scalar product (you've already got it):
function scalarProduct(a, b) {
let res = 0;
for(let i = 0; i < a.length; i++) {
res += a[i] * b[i];
}
return res;
}
and then map it over your matrix (array of vectors):
result = xValuesMinusMean.map(vec => scalarProduct(vec, yValuesMinusMean))
You can have one reduce function where you will take product of arrays and store it in accumulator.
const val =[ [ 2.75,3.75,6.75,5.25 ], [-2,-1,2,1]];
const yvalues = [-5, -4, -1, 10];
console.log(val.map(o=>o.reduce((a,e,i)=>a+=e*yvalues[i],0)));
Looks like your calculation is not correct first set of arrays will also return 22.
Live Demo :
const yValuesMinusMean = [-5, -4, -1, 10];
const xValuesMinusMean = [[2.75, 3.75, 6.75, 5.75], [-2, -1, 2, 1]];
const finalArr = [];
xValuesMinusMean.forEach(arr => {
let cal = 0;
arr.forEach((item, index) => {
cal += item * yValuesMinusMean[index]
});
finalArr.push(cal);
});
console.log(finalArr); // [22, 22]

Is there a way to create dynamic ranges given an array of values, and counting how many belong to each range

So i have a frontend with a Piechart and want to show the percentage of the ages in my customers (a database table). I've stored the age of each customer so i have an array like this.
const ages = [12,42,23,42,12,65,75,12,43,54,12,53,24,23,54,64,76,12,42];
Given these values, i want to end up having somenthing like this
const data = {
labels: ['12-20', '21-40', '41-60', '61-76']
dataSet: [4, 6, 2, 5] // This is the amount of ages between each range. The sum of these must be equivalent of the length of the array
}
This is what i've tried so far
const ages = [12, 42, 53, 12, 32, 12, 52, 66, 76, 87, 23, 12, 43, 12, 43, 54, 65].sort((a, b) => a - b);
const minAge = Math.min(...ages);
const maxAge = Math.max(...ages);
const q1 = ages[Math.floor(ages.length / 4)];
const q2 = ages[Math.floor(ages.length / 2)];
const q3 = ages[Math.floor(ages.length * 3 / 4)];
let firstRangeCount = 0;
let secondRangeCount = 0;
let thirdRangeCount = 0;
let fourthRangeCount = 0;
for (const age of ages) {
if (age) {
if (age <= q1) {
firstRangeCount++;
} else if (age <= q2) {
secondRangeCount++;
} else if (age <= q3) {
thirdRangeCount++;
} else {
fourthRangeCount++;
}
}
}
const data = {
labels: [
`${minAge} - ${q1}`,
`${q1} - ${q2}`,
`${q2} - ${q3}`,
`${q3} - ${maxAge}`,
],
datasets: {
label: 'Ages',
data: [firstRangeCount, secondRangeCount, thirdRangeCount, fourthRangeCount],
}
}
But the problem with this solution that it isnt dynamic. If the ages array contains less data, 4 ranges wouldn´t be appropiated.
How can i make this ranges "dynamic"?. I've read some about interquartile range but it didn´t help me much
Try this (Descriptive comments has been added in the below code snippet) :
// Input array
const ages = [12,42,23,42,12,65,75,12,43,54,12,53,24,23,54,64,76,12,42];
// data object with range array
const data = {
labels: ['12-20', '21-40', '41-60', '61-76'],
dataSet: []
}
// declare a variable which will contain the count of occurance based on the range
const obj = {};
// logic to find out the counts and pushed into an range array in an object.
data.labels.forEach(label => {
const splittedLabel = label.split('-')
const filteredAges = ages.filter(e => e >= splittedLabel[0] && e <= splittedLabel[1]);
obj[label] = [];
obj[label].push(...filteredAges);
});
// getting the length
const dataSet = Object.values(obj).map(arr => arr.length);
data.dataSet = dataSet;
// Result
console.log(data);

Javascript - Updating duplicated numbers in an array

I have an unordered list like this;
list1 = [2,3,1,2,1,3,3,1,2]
I want to add +10 to the recurring elements in this list every time. So there should be a list as follows;
list1 = [2,3,1,12,11,13,23,21,22]
At the same time, the list order must remain intact.
In fact, the list is longer than the example here (10 digits repeat 7 times).
I would be grateful for your suggestions.
Just have a lookup which count the frequency of number and then multiply the frequency with 10 and add the number.
const list1 = [2,3,1,2,1,3,3,1,2],
lookup = {},
result = list1.map(number => {
lookup[number] = (lookup[number] || 0) + 1;
return number + (lookup[number] - 1) * 10;
});
console.log(result);
You can do it like this:
var list1 = [2,3,1,2,1,3,3,1,2];
var countObject = {};
list1.forEach(function(item, index) {
if(countObject[item]) {
list1[index] = item + (10 * countObject[item]);
countObject[item] = countObject[item] + 1;
} else {
countObject[item] = 1;
}
});
For a compact version, you could take a closure over an object for counting and assign zero to unknown property and increment this value.
const
list = [2, 3, 1, 2, 1, 3, 3, 1, 2],
result = list.map((o => v => v + 10 * (o[v] ??= 0, o[v]++))({}));
console.log(...result);

Split an array of items into several arrays, given a condition

The question is very simple : I have an array of items, that all have a date. According to the date, I would like to split this array into several arrays.
In my case, I would like to split the array into 3 arrays : one that contains result of the last 24h, the other 48h, then the last one on the week.
Here is a minimal reproduction of the case, with the final solution.
const now = Date.now();
const day = 1000 * 3600 * 24;
const todayTime = now - day;
const yesterdayTime = now - day * 2;
const weekTime = now - day * 7;
const arr = [
{ id: 1, date: todayTime + 100 },
{ id: 2, date: yesterdayTime + 100 },
{ id: 3, date: weekTime + 100 },
];
const today = arr.filter(item => item.date - todayTime > 0);
const yesterday = arr.filter(item => item.date - yesterdayTime > 0 && item.date - todayTime < 0);
const week = arr.filter(item => item.date - weekTime > 0 && item.date - yesterdayTime < 0);
console.log(today, yesterday, week);
Is there a way to make it more concise ? I'm talking about code length : I would like to reduce it to the maximum, especially the definition of the 3 arrays. I tried with reduce, but the syntax is pretty ugly.
Any ideas ?
It is not shorter, but I would go for a more generic function which groups data into chunks based on a given property ("date" in your case) and some boundary values (you have 3), provided in ascending order. Then that function would return one more value, since 3 boundaries split an infinite range into 4 ranges, ...etc.
So this is what that function would look like:
function groupInRanges(data, prop, boundaries) {
// NB: boundaries must be sorted ascending.
return data.reduce((acc, item) => {
let index = boundaries.findIndex(boundary => item[prop] < boundary);
if (index < 0) index = boundaries.length;
acc[index].push(item);
return acc;
}, [[], ...boundaries.map(() => [])]);
}
// Demo
const now = Date.now();
const day = 1000 * 3600 * 24;
const arr = [
{ id: 1, date: now - day + 100 },
{ id: 2, date: now - 2*day + 100 },
{ id: 3, date: now - 5*day },
];
// We are not interested in the first value (data older than 1 week):
const [, week, yesterday, today] = groupInRanges(arr, "date", [now-day*7, now-day*2, now-day]);
console.log(today, yesterday, week);
Note that if your data could possibly have future dates, you would need one extra boundary set to now.
More specific solution
This assumes that there is no data older than a week or future. Use a ternary operator to determine in which array a data item should be pushed:
const now = Date.now();
const day = 1000 * 3600 * 24;
const todayTime = now - day;
const yesterdayTime = now - day * 2;
const weekTime = now - day * 7;
const arr = [
{ id: 1, date: todayTime + 100 },
{ id: 2, date: yesterdayTime + 100 },
{ id: 3, date: weekTime + 100 },
];
const [today, yesterday, week] = arr.reduce((acc, item) => {
acc[item.date > todayTime ? 0 : item.date > yesterdayTime ? 1 : 2].push(item);
return acc;
}, [[], [], []]);
console.log(today, yesterday, week);
A tiny bit shorter, but less readable, with the comma operator and expression arrow-function syntax:
const [today, yesterday, week] = arr.reduce((acc, item) =>
(acc[item.date > todayTime ? 0 : item.date > yesterdayTime ? 1 : 2].push(item), acc)
, [[], [], []]);
You could move the function into an array and get the index for later pushing the item.
const
update = (array, index, value) => {
if (index !== -1) (array[index] = array[index] || []).push(value);
return array;
},
now = Date.now(),
day = 1000 * 3600 * 24,
todayTime = now - day,
yesterdayTime = now - day * 2,
weekTime = now - day * 7,
arr = [{ id: 1, date: todayTime + 100 }, { id: 2, date: yesterdayTime + 100 }, { id: 3, date: weekTime + 100 }],
fns = [
date => date - todayTime > 0,
date => date - yesterdayTime > 0 && date - todayTime < 0,
date => date - weekTime > 0 && date - yesterdayTime < 0,
],
result = arr.reduce(
(r, item) => update(r, fns.findIndex(f => f(item.date)), item),
fns.map(_ => [])
);
console.log(result);
You want to make following part more concise right?
const today = arr.filter(item => item.date - todayTime > 0);
const yesterday = arr.filter(item => item.date - yesterdayTime > 0 && item.date - todayTime < 0);
const week = arr.filter(item => item.date - weekTime > 0 && item.date - yesterdayTime < 0);
Here's what I've tried:
const [today, yesterday, week] = arr.reduce((result, item) => (index = item.date - todayTime > 0 ? 0 : item.date - yesterdayTime > 0 && item.date - todayTime < 0 ? 1 : 2, result[index].push(item), result), [[], [], []])
Final code is:
const now = Date.now();
const day = 1000 * 3600 * 24;
const todayTime = now - day;
const yesterdayTime = now - day * 2;
const weekTime = now - day * 7;
const arr = [
{ id: 1, date: todayTime + 100 },
{ id: 2, date: yesterdayTime + 100 },
{ id: 3, date: weekTime + 100 },
];
// revised
const [today, yesterday, week] = arr.reduce((result, item) => (index = item.date - todayTime > 0 ? 0 : item.date - yesterdayTime > 0 && item.date - todayTime < 0 ? 1 : 2, result[index].push(item), result), [[], [], []])
console.log(today, yesterday, week)
Here is another option using map instead of reduce like most of the other answers.
This simply works by comparing the dates, removing the item from the array if it matches a certain criteria. In the example below I duplicate the array leaving the old arr intact. However you can simply ignore this if you don't care about arr mutating, or if you'd like to see what items didn't match any criteria (since they are left behind).
The reason I need a reverseEach function is obvious. If I start deleting items from an array that I'm currently iterating over the elements start to shift. When iterating from back to front this doesn't matter since I've already handled those items.
In the example below, order matters. When you put t1w (time 1 week) as first element in the array all the elements of arr will match the criteria and will be present in that group. Make sure you provide the criteria incremental (or decremental depending on the context).
// define some helpers
Array.prototype.deleteAt = function (index) { return this.splice(index, 1)[0]; };
Array.prototype.dup = function () { return this.slice(0); };
Array.prototype.reverseEach = function (callback, thisArg) {
if (thisArg !== undefined) callback = callback.bind(thisArg);
for (let index = this.length - 1; index >= 0; index -= 1) {
callback(this[index], index, this);
}
};
// initialisation
const now = Date.now();
const day = 1000 * 3600 * 24;
const t1d = now - day;
const t2d = now - day * 2;
const t1w = now - day * 7;
const arr = [
{ id: 1, date: t1d + 100 },
{ id: 2, date: t2d + 100 },
{ id: 3, date: t1w + 100 },
];
// actual answer
const groups = [t1d, t2d, t1w].map(function (limit) {
const group = [];
this.reverseEach((item, index) => {
if (item.date > limit) group.unshift(this.deleteAt(index));
});
return group;
}, arr.dup());
console.log(groups);

Group multiple overlapping timeblocks that may not have a direct overlap

I am trying to group timeslots by overlap but I can't figure out how to do it exactly.
I have a pretty simple array in the form of [{start_at: Date, end_at: Date, etc.etc. }]
And I lay them out in my view like this
<---slot1----><----slot5----><--slot6-->
<--slot2-><--slot4---> <--slot7-->
<----slot3---->
Finding directly overlapping slots isn't that hard, I just compare a slot with the next one with (StartA <= EndB) and (EndA >= StartB)
from here.
Now I want to group my overlapping slots (slot 1, 2, 3, 4 and 5) but not include slot 6 and 7, and put those two in their own group. into something like [[Slot (has 1 through 5)][Slot (has 6 and 7)]]
I am kind of lost with this problem right now and I hope anybody here can help me.
I'd suggest creating a Slot object that holds:
an array of items in the slot,
the earliest start_at date of those items,
the latest end_at of those items.
By keeping an up to date slot-range, you don't have to compare a new item to each of the slot's items. You'll only have to compare to the slot itself.
Now, you'll have to sort your items by start_at. You can then reduce the array by:
Create a Slot for the first item
Set the Slot's start_at and end_at to mimic those of the first item
Go to the second item, check for overlap with the first Slot
If it overlaps,
push the second item to the Slot's items array, and
Set start_at to the minimum of Slot.start_at and item2.start_at
Do the same (max) for end_at
If it does not overlap,
Create a new Slot for the second item, repeat with this Slot and item3 (et cetera)
A sample implementation (I'd advice you to rewrite it based on your personal preferences. I didn't make any neat classes/prototypes/etc., nor did I test it thoroughly)
function createSlot(initialItem) {
var slot = {
items: [initialItem],
start: initialItem.start,
end: initialItem.end
};
slot.addItem = function(item) {
slot.items.push(item);
slot.start = Math.min(slot.start, item.start);
slot.end = Math.max(slot.end, item.end);
}
return slot;
};
function itemsOverlap(item1, item2) {
return item1.start <= item2.end &&
item1.end >= item2.start;
};
var slots = [];
var items = randomItems(10);
items.slice(1).reduce(function(currentSlot, item) {
if (itemsOverlap(currentSlot, item)) {
currentSlot.addItem(item);
return currentSlot;
}
slots.push(currentSlot);
return createSlot(item);
}, createSlot(items[0]));
console.log(
slots.map(function(slot) { return slot.items.length; }));
// Create random data
function randomItems(n) {
var arr = [];
for (var i = 0; i < n; i += 1) {
arr.push(generateRandomItem());
}
return arr.sort(function(a, b) { return a.start - b.start; });
};
function randomHourTimespan() {
return Math.random() * 60 * 60 * 1000;
};
function randomHalfDayTimespan() {
return randomHourTimespan() * 12;
};
function generateRandomItem() {
var start = Date.now() + randomHalfDayTimespan();
var end = start + randomHourTimespan();
return { start: new Date(start), end: new Date(end) };
}
I implemented a simple algorithm to group the slots regarding to the start and end values.
Here is a working fiddle https://jsfiddle.net/LeoAref/gg6q0mby/, and you will find a visual presentation for the grouping.
var timeSlots = [
{start: 0, end: 3},
{start: 1, end: 2},
{start: 2, end: 4},
{start: 4, end: 6},
{start: 4, end: 8},
{start: 5, end: 6}
];
timeSlots.forEach((slot, index) => {
var slotElem = document.createElement('div');
slotElem.classList.add('slot');
slotElem.style.top = index * 25 + 'px';
slotElem.style.left = slot.start * 30 + 'px';
slotElem.style.width = (slot.end - slot.start) * 30 + 'px';
document.body.appendChild(slotElem);
});
var groups = [];
timeSlots.forEach(slot => {
added = false;
if (groups.length) {
var index = 0;
do {
group = groups[index];
if (slot.start >= group.start && slot.start < group.end ||
slot.end <= group.end && slot.end > group.start
) {
group.slots.push(slot);
group.start = Math.min(slot.start, group.start);
group.end = Math.max(slot.end, group.end);
added = true;
}
} while (!added && ++index < groups.length);
if (!added) {
groups.push({start: slot.start, end: slot.end, slots: [slot]});
}
} else {
groups.push({start: slot.start, end: slot.end, slots: [slot]});
}
})
groups.forEach(group => {
var groupElem = document.createElement('div');
groupElem.classList.add('group');
groupElem.style.left = group.start * 30 + 'px';
groupElem.style.width = (group.end - group.start) * 30 - 2 + 'px';
document.body.appendChild(groupElem);
})
#user3297291's description/algorithm of a time interval grouping function is really good. Here's a function that was created/posted on GitHub by the user 'blaston' from several years ago that follows the algorithm. I'm posting it here in case the content/link disappears. I started with blaston's function for its simplicity to follow and swapped array groups in blaston's function for slot objects from #user3297291's post.
// Group all overlaping intervals
// * * * * * * *
// This is an approach to a problem the engineers at Google Calandar/ Outlook probably faced.
// You have events that may overlap and you want to display them in such a way that
// they don't overlap with each other. One approach is to distribute them into columns.
// Each column has events that don't overlap with each other.
// Cost: O(n*log n) if the interval aren't sorted by the starting time,
// O(n) otherwise.
// Sample run: groupOverlapingIntervals([ [2, 5], [5, 6],[3, 4] ])
// Output: [ [ [2, 5], [3, 4], [5, 6] ] ]
function groupOverlapingIntervals(intervals) {
intervals.sort(function(a, b) {
return a[0] - b[0];
});
var groups = [
[intervals[0]]
];
var j = 0;
var end = intervals[0][1];
for (var i = 1; i < intervals.length; i++) {
if (intervals[i][0] <= end) {
if (intervals[i][1] > end) {
end = intervals[i][1];
}
groups[j].push(intervals[i]);
} else {
groups.push([intervals[i]]);
j++;
end = intervals[i][1];
}
}
return groups;
}
var intervals = [
[2, 5],
[5, 6],
[3, 4],
[7, 8],
[6.5, 9],
[10, 11.5]
];
var groups = groupOverlapingIntervals(intervals);
console.log(groups);

Categories