Summing two ordered arrays of accumulative object values over time - javascript
I'm trying to figure out how to sum accumulatives values over time of two arrays, seems simple, but here's what complicates it: there might be missing dates from one of them.
When one has a value for a date and the other doesn't, we sum together that value that exists from one array and the last-seen (previous) value for a date of the other array (The example I give demonstrates this better).
Example, given two arrays of objects where in data2 there's a value for a date that data1 doesn't have:
var data1 = [
{date: "30-08-2019", value: 1},
{date: "03-09-2019", value: 2},
{date: "04-09-2019", value: 3}
]
var data2 = [
{date: "30-08-2019", value: 1},
{date: "02-09-2019", value: 2},
{date: "03-09-2019", value: 3},
{date: "04-09-2019", value: 4}
]
I want the result of summing these two together (data1 + data2) to be:
var result = [
{date: "30-08-2019", value: 2}, //{date: "30-08-2019", value: 1} + {date: "30-08-2019", value: 1}
{date: "02-09-2019", value: 3}, //{date: "30-08-2019", value: 1} + {date: "02-09-2019", value: 2}
{date: "03-09-2019", value: 5}, //{date: "03-09-2019", value: 2} + {date: "03-09-2019", value: 3}
{date: "04-09-2019", value: 7} //{date: "04-09-2019", value: 3} + {date: "04-09-2019", value: 4}
]
Since both arrays are ordered, the approach I thought was looping the array with more data and sum it with the values of the array with less data, keeping track of what are the last date values that the smaller data gives, like this:
for(let i = 0; i < biggerData.length; i++){
//both have values for a date that exists the in bigger date array, so we sum them together
if(smallerData[i][biggerData[i].date]){
biggerData[i].value+=smallerData[i][biggerData[i].date];
lastValue = smallerData[i][biggerData[i].date];
//array with less data has a missing date, then sum the last saved value it gave
}else{
biggerData[i].value+=lastValue;
}
}
There's a problem with this approach, what if the smaller array has a date that the bigger one doesn't? That value one won't be added to the final result in this case.
When going more further than this I started to loop one array like I showed before and then I loop over the other one to get the missing dates, but this just seems too complex and inefficient. I'm pretty sure there's a solution for doing this in one loop (or even not using loops at all).
I'm asking if anybody can figure out a better solution for this, I'm making this in JavaScript.
I used a bunch of helper variables and converted the dates into an easily sortable format. Going through all existing dates in chronological order makes it quite easy to keep track of the accumulated value for each array. The sorting is the inefficient part, since the rest has linear complexity. You could optimize the sorting by taking advantage of the fact that both arrays are already sorted, but I was too lazy to do this here :)
// Turn '30-08-2019' into '2019-08-30'
const getSortableDate = (dateString) => dateString.split('-').reverse().join('-');
// Enable direct lookup of values
const mapDatesToValues = (data) => {
const dates = {};
data.forEach((item) => {
dates[getSortableDate(item.date)] = item.value;
});
return dates;
};
// Source data
const data1 = [
{date: "30-08-2019", value: 1},
{date: "03-09-2019", value: 2},
{date: "04-09-2019", value: 3}
];
const data2 = [
{date: "30-08-2019", value: 1},
{date: "02-09-2019", value: 2},
{date: "03-09-2019", value: 3},
{date: "04-09-2019", value: 4}
];
// values for direct lookup
const dates1 = mapDatesToValues(data1);
const dates2 = mapDatesToValues(data2);
// Chronological order for all existing dates
const allDatesOrdered = Object.keys({ ...dates1, ...dates2 }).sort();
// Helper variables:
let acc1 = 0; // Accumulated value while iterating through data1
let acc2 = 0; // Accumulated value while iterating through data2
let existsIn1;
let existsIn2;
let value1; // Current value while iterating through data1
let value2; // Current value while iterating through data2
allDatesOrdered.forEach((date) => {
existsIn1 = dates1.hasOwnProperty(date);
existsIn2 = dates2.hasOwnProperty(date);
value1 = dates1[date];
value2 = dates2[date];
// Remember accumulated values
if (existsIn1) {
acc1 = value1;
}
if (existsIn2) {
acc2 = value2;
}
if (existsIn1 && existsIn2) {
console.log('sum for', date, 'is', value1 + value2, '(found in both arrays)');
} else {
if (existsIn1) {
console.log('sum for', date, 'is', value1 + acc2, '(only found in data1)');
} else {
console.log('sum for', date, 'is', value2 + acc1, '(only found in data2)');
}
}
});
Figured out a way of going through this quite efficiently, but I converted the dates to millisecond timestamps due to beeing more suitable in the context I'm using this. Because of this change I'm not going to put my answer as the correct one.
#timotgl answer does it without converting date values so therefore I'm marking it the correct answer, although the solution also contains a change in the date format (that didn't helped me in my case but can help others).
I'm basically doing a zip function, going through both arrays and the merged result is being pushed in a result array of objects in one go.
data1.forEach((item) => {
item.date = new Date(item.date).getTime();
});
data2.forEach((item) => {
item.date = new Date(item.date).getTime();
});
let mergedPortfolio = [], //final array of objects
data1Idx = 0, //indexes for each array of objects
data2Idx = 0,
data1Last, //keeping track of last values
data2Last,
date1, //current date value
date2,
value1,//current value
value2;
while(data1Idx < data1.length || data2Idx < data2.length){
//both arrays exist
if(data1Idx < data1.length && data2Idx < data2.length){
date1 = data1[data1Idx].date;
date2 = data2[data2Idx].date;
value1 = data1[data1Idx].value;
value2 = data2[data2Idx].value;
if(date1 < date2){
mergedPortfolio.push({date: date1, value: value1+data2Last});
data1Last = value1;
++data1Idx;
}else if(data1[data1Idx].date === data2[data2Idx].date){
mergedPortfolio.push({date: date1, value: value1+value2})
data1Last = value1;
data2Last = value2;
++data1Idx;
++data2Idx;
}else if(data1[data1Idx].date > data2[data2Idx].date){
mergedPortfolio.push({date: date2, value: data1Last+value2});
data2Last = value2;
++data2Idx;
}
//Working through the remaining items in one data1 array
}else if(data1Idx < data1.length){
date1 = data1[data1Idx].date;
value1 = data1[data1Idx].value;
mergedPortfolio.push({date: date1, value: value1+data2Last});
data1Last = value1;
++data1Idx;
//Working through the remaining items in the data2 array
}else if(data2Idx > data2.length){
date2 = data2[data2Idx].date;
value2 = data2[data2Idx].value;
mergedPortfolio.push({date: date2, value: value2+data1Last});
data2Last = value1;
++data2Idx;
}
}
Related
Javascript runtime optimizing array modifications
I have a problem optimizing the runtime for my code. Ideally, what I'm trying to achieve is that all the operations below is performed in a single loop, so that I don't have to run through the dataset many times as I'm doing now (very large dataset!) The code is transforming aggData to an array on the following format: [0: 0, 1: 0, 2: 0, 3: 43, 4: 121, 5: 0, ....], where each number represents a year in the interval, if the interval is (1800-2020) 0 will represent the count for 1800, 1 will be 1801 and so on .. aggData is an array of objects on the following format: {key_as_string: "1900-01-01T00:00:00.000Z", key: -2208988800000, doc_count: 17}. The start-year is the first year with a doc_count higher than 0. Below I provide a description of what each step does as the code is now: Here I am changing the format of each object in the list to be : {year: number, count: number} const formatAggData = aggData .map((item: AggData) => { return { year: moment(item.key_as_string).year(), count: item.doc_count }; }); This function creates an array of objects with the from as start year and to as end year, if the year already exists in existingArray it uses the count from there, if not it sets count to 0. function fillYears(from: number, to: number, existingArray: YearCount[]) { const existingObject: { [year: string]: number } = { year: null }; existingArray.map((x: YearCount) => (existingObject[x.year] = x.count)); const yearsArray = []; for (let i = from; i <= to; i++) { yearsArray.push({ year: i, count: existingObject[i] || 0, }); } return yearsArray; } Converts year values to count-values, where the first year in the list will be 0 with the corresponding value, second will be 1 with corresponding value and so on.. const resultList = fillYears(min, max, formatAggData).map( (item: YearCount) => item.count, );
I was looking at you code. can't you do it like this? it looks like you don't need to know the year at this moment function fillYears(from: number, to:number, existingYears: YearCount[]) { for (let i = from; i <= to; i++) { yearsArray.push({ year: i, count: existingYears[i].doc_count || 0, }); } }
Search and replace value of object property from one array with value of object property from second array?
I have two arrays of objects say 1- variants and 2- inventoryLevels. Objects in both arrays share a property which is the id. So I want to search for each variant if it's id is matched with any inventoryLevel I want to change its property named shopify_inventory_quantity with matched inventoryLevel's property available ? My words are little but confusing but take a look at code below basically it's doing properly whats needed I just want to know can it be optimized right now it's nested for loop. So any help to make it efficient would be appreciated ? for (let i = 0; i < variants.length; i++) { for (let j = 0; j < inventorylevels.length; j++) { if (variants[i].id === inventorylevels[j].variant_id) { variants[i].shopify_inventory_quantity = inventorylevels[j].available; } } }
I understand you have a solution in O(n²). Assuming your ids are unique, you can reduce the time complexity to O(n) (basically what #Alireza commented): var variants = [ {id: 0, shopify_inventory_quantity: 0}, {id: 1, shopify_inventory_quantity: 0}, {id: 2, shopify_inventory_quantity: 0} ]; var inventoryLevels = [ {id: 0, available: 10}, {id: 1, available: 2}, {id: 2, available: 3} ]; // O(n) + O(n) = O(n) function getAvailableVariants(v, i) { // O(n) var inventoryLevels = i.reduce(function(inventoryLevels, inventoryLevel) { inventoryLevels[inventoryLevel.id] = inventoryLevel; return inventoryLevels; }, {}); // O(n) return v.map(variant => Object.assign(variant, {shopify_inventory_quantity: inventoryLevels[variant.id].available})); } var results = document.createElement('pre'); results.textContent = JSON.stringify(getAvailableVariants(variants, inventoryLevels), null, '\t'); document.body.appendChild(results);
Give objects each a set random atribute for objects in an array
I have 30 objects in an array. One of the attributes in each object is question_order. I want to make a javascript function that gives an order to the objects. To do this I would need to give each object's attribute question_order a number of 1-30 and each question_order number would have to be unique. What would be the best way to go about this? Thanks!
You can create an array of indexes and shuffle if. Then you can loop through your items and assign a question_order based on the shuffled array: function shuffle(array) { var index = array.length; while (--index >0 ) { let randIndex = Math.floor(Math.random() * index); [array[index], array[randIndex]] = [ array[randIndex], array[index]]; } return array; } let items =[ {name: 0}, {name: 1}, {name: 2}, {name: 3}, {name: 4} ] // make an index arr i.e [0,1,2,3] the same length as items let indexes = shuffle(Array.from({length: items.length}, (_,i) => i)) // add question order items.forEach((item, index) => item.question_order = indexes[index]) console.log(items) But if you can already shuffle and array, why not just shuffle the original array and save yourself the extra work: function shuffle(array) { var index = array.length; while (--index >0 ) { let randIndex = Math.floor(Math.random() * index); [array[index], array[randIndex]] = [ array[randIndex], array[index]]; } return array; } let items =[ {name: 0}, {name: 1}, {name: 2}, {name: 3}, {name: 4} ] console.log(shuffle(items))
d3.js group by time period
How can i nest data by same sized timespans like decades. From this [{key: "1432"} {key: "1436"} {key: "1557"} {key: "1559"} {key: "1834"} {key: "1836"} {key: "1839"} {key: "undefined"}] To this [{key: "1430-1439", values: Array} {key: "1550-1559", values: Array} {key: "1830-1839", values: Array} {key: "undefined", values: Array}] Additionally it would be nice if the size of the timespan is calculated dynamically.
I solved the problem by looping over all of the elements and add the corresponding decade to it like here. for(i = 0; i < dataToDraw.length; i++) { if(dataToDraw[i].year != undefined) { var decadeStart = dataToDraw[i].year - dataToDraw[i].year % 10; dataToDraw[i].decade = decadeStart + "-" + (decadeStart + 9); } } After that I used d3.nest to nest the data by the new decade field. nestedDecadeData = d3.nest() .key(function(d) { return d.decade; }) .sortKeys(d3.ascending) .entries(dataToDraw);
Sort array into "rows" in JavaScript
I have an array of objects that is currently like this, in which entries are ordered by date and time: var checkin_data = [ {id: 430, date: "2013-05-05", time: "08:24"}, {id: 435, date: "2013-05-06", time: "04:22"}, {id: 436, date: "2013-05-06", time: "05:36"}, {id: 437, date: "2013-05-06", time: "07:51"}, {id: 488, date: "2013-05-06", time: "08:08"}, {id: 489, date: "2013-05-06", time: "10:12"}, {id: 492, date: "2013-05-06", time: "13:18"}, {id: 493, date: "2013-05-06", time: "15:55"}, {id: 494, date: "2013-05-06", time: "18:55"}, {id: 498, date: "2013-05-06", time: "22:15"}, {id: 501, date: "2013-05-07", time: "11:40"}, {id: 508, date: "2013-05-07", time: "18:00"}, {id: 520, date: "2013-05-08", time: "04:48"}, {id: 532, date: "2013-05-09", time: "21:11"}, {id: 492, date: "2013-05-10", time: "11:45"}, {id: 601, date: "2013-05-11", time: "18:12"} ]; The dates represent a date in a particular week: I'd like to sort this array in order to lay it out in "rows", so the data needs to be re-sorted to lay out like this (note the order of the dates): var checkin_data = [ {id: 430, date: "2013-05-05", time: "08:24"}, {id: 435, date: "2013-05-06", time: "04:22"}, {id: 501, date: "2013-05-07", time: "11:40"}, {id: 520, date: "2013-05-08", time: "04:48"}, {id: 532, date: "2013-05-09", time: "21:11"}, {id: 492, date: "2013-05-10", time: "11:45"}, {id: 601, date: "2013-05-11", time: "18:12"}, {id: 436, date: "2013-05-06", time: "05:36"}, {id: 508, date: "2013-05-07", time: "18:00"}, {id: 437, date: "2013-05-06", time: "07:51"}, {id: 488, date: "2013-05-06", time: "08:08"}, {id: 489, date: "2013-05-06", time: "10:12"}, {id: 492, date: "2013-05-06", time: "13:18"}, {id: 493, date: "2013-05-06", time: "15:55"}, {id: 494, date: "2013-05-06", time: "18:55"}, {id: 498, date: "2013-05-06", time: "22:15"} ]; Getting the data in that order would allow me to lay out a table like this: Thanks, any help would be appreciated.
Here is a suggestion using functional methods: Reduce the list into arrays of buckets based on day, and sort that list (this is like reading the table you've got on rows) Iterate through the rows in order, clear out unused ones. Here: //first, we collapse the array into an array of buckets by day half_sorted = checkin_data.reduce(function(accum,cur){ var bucket = new Date(cur.date).getDay(); accum[bucket].push(cur); return accum; },[[],[],[],[],[],[],[]]).map(function(day){ return day.sort(function(x,y){ // now we sort each bucket return new Date("01-01-1990 "+x.time) - new Date("01-01-1990 "+y.time); }); }); // At this point, we have an array with 7 cells looking like your table // if we look at its columns. // finally, we push to the result table. var result = []; var daysToClear = 7; for(var i=0;daysToClear>0;i=(i+1)%7){ if(half_sorted[i] && half_sorted[i].length > 0){ result.push(half_sorted[i].pop()); }else if(half_sorted[i] && half_sorted[i].length === 0){ half_sorted[i] = null; daysToClear--; } } Working fiddle
First of all, I think you're going about this in the wrong way. Please see my note below the following code. To do exactly as you've asked, here's one way: // parsing the date strings ourselves avoids time zone problems function dateFromString(string) { var parts = string.split('-'); return new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)); } The above is a utility function. var i, l, dates = [[], [], [], [], [], [], []], item; // place the objects into dow-sorted buckets for (i = 0, l = checkin_data.length; i < l; i += 1) { item = checkin_data[i]; dates[dateFromString(item.date).getDay()].push(item); } i = 0; l = 0; checkin_data = []; while (true) { // instead of a for loop to handle the row-wrap manually if (dates[i][l]) { item = dates[i][l]; checkin_data.push(item); } i += 1; if (i === 7) { if (!item) { break; // we had a complete row with no data } item = undefined; l += 1; i = 0; } } checkin_data is now sorted in the order you requested. Note: you really don't need the second loop, because it is doing most of the work you'll have to do again to use the provided array. So in your routine for writing out the table, instead just use the data structure that the first loop creates. You would of course need a slightly different bailout strategy since you don't want to create an extra blank row, but I'll leave that up to you. After a bit of thought, though, I came up with another way to do it, if you don't mind adding a new key to your objects: function dateFromString(string) { var parts = string.split('-'); return new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)); } var i, l, counts = [0, 0, 0, 0, 0, 0, 0], item, dow; for (i = 0, l = checkin_data.length; i < l; i += 1) { item = checkin_data[i]; dow = dateFromString(item.date).getDay(); item.sortKey = ++counts[dow] * 7 + dow; } checkin_data.sort(function(a, b) { return a.sortKey - b.sortKey; });
I've come up with a solution, maybe not the most elegant, but it's working: var sorted_data = [], elements_to_dump = [], i, j, tmp; while (checkin_data.length > 0) { for (i = 0; i < checkin_data.length; i++) { if (checkin_data[i-1]) { if (checkin_data[i-1].date === checkin_data[i].date) { continue; } } sorted_data.push(checkin_data[i]); elements_to_dump.push(checkin_data[i].id); } for (j = 0; j < elements_to_dump.length; j++) { for (i = 0; i < checkin_data.length; i++) { if (checkin_data[i].id === elements_to_dump[j]) { tmp = checkin_data.splice(i, 1); break; } } } }
I'd like to sort this array in order to lay it out in "rows", so the data needs to be re-sorted [into this linear representation]. Getting the data in that order would allow me to lay out a table No, it does not need to be. Actually, that's one step too much, and the order of your intermediate result makes absolutely no sense. What you should do instead is construct a (weekday-) list of (entries-per-day-) lists: var days = []; for (var i=0, date=null, day; i<checkin_data.length; i++) { var entry = checkin_data[i]; if (entry.date !== date) days.push(day = []); day.push(entry); } That's it, you have you two-dimensional format now. Well, maybe you will need to transpose it to get it into the table you wanted, but that's not too complicated either: var header = [], table = [header]; // or create a DOM or a HTML string or whatever for (var i=0; i<days.length; i++) header.push(days[i][0].date /* or the weekday name? */); for (var r=0; !done; r++) { var row = [], done = true; // create cells: for (var i=0; i<days.length; i++) if (days[i].length > r) { row[i] = days[i][r].time; done = false; } else row[i] = ""; } if (!done) table.push(row); }
What you're trying to do is very simple. This is what I would do: var groups = groupBy(checkin_data, "date"); // groups items based on date var table = zipAll.apply(null, toArray(groups)); // zips corresponding elements // of each group into an array After this you may create your table as follows: var header = getHeader(groups), rows = map(table, getRow); document.body.appendChild(getTable(header, rows)); Of course the actual code would be much bigger (a little more than 100 lines of code) since you need to write the logic for groupBy, toArray, zipAll, map, getHeader, getRow, getTable, etc. Luckily for you I had the time to go and write all this stuff. Hence you now have a working demo: http://jsfiddle.net/hZFJw/ I would suggest that you browse through my code and try to understand how it works. It's too much to explain in one answer. Note: My solution may be more than 100 lines of code. However it's still a simple solution because: The actual code which generates the table is just 4 lines long and is very simple to understand. The rest of the code is composed of reusable functions like groupBy, zipAll and map. These functions are very small and simple to understand. Overall by abstracting the program into reusable functions the size of the program has increased. However the complexity of the program has considerably decreased. You could achieve the same result by tackling the problem in an imperative style like most other answers do. However doing so makes the program harder to understand. Compare my code with other answers and see for yourself.
You can sort array with Alasql JavaScript library. alasql.fn.WEEKDAY = function(d) { // User-defined function return (new Date(d)).getDay(); }; var res = alasql('SELECT *, WEEKDAY(date) AS dow FROM ? ORDER BY dow', [checkin_data]); Try this example at jsFiddle.