Merge arrays with overlapping values - javascript

Im using Node.js. (...and underscore.js)
Consider this data structure
var numbers = [
[10, 20]
[30, 40]
[40, 50]
[45, 70]
... //Possibly more arrays (always contains two numbers)
]
numbers contain arrays that always contain number pairs. Think of these number pairs as "start" and "end". I want a function that takes numbers as argument, and loop trough its content, and if the "start" number of a pair overlap the "end" number of previous pair, these arrays is merged into one. For example this:
var numbers = [
[10, 20]
[19, 40]
[40, 60]
[70, 80]
]
Becomes this:
var numbers = [
[10, 60] // First, second and third array is merged because of overlapping .
[70, 80]
]
Actually, I already have written a function for this that works fine, but feels a bit clunky.
I'm curious if some javascript wizard can dazzle me with a super elegant solution =).

Create an empty "result" array. Loop over the ranges array and either change the last item of the result or add the current range to it.
function merge(ranges) {
var result = [], last;
ranges.forEach(function (r) {
if (!last || r[0] > last[1])
result.push(last = r);
else if (r[1] > last[1])
last[1] = r[1];
});
return result;
}
r = [[10, 20], [19, 40], [40, 60], [70, 80]];
document.write(JSON.stringify(merge(r)));
This assumes that the source array is sorted, if it's not always the case, sort it before merging:
ranges.sort(function(a, b) { return a[0]-b[0] || a[1]-b[1] });

I created a function which does what you want:
function merge(arr) {
// copy and sort the array
var result = arr.slice().sort(function(a, b) {
return a[0] > b[0];
}),
i = 0;
while(i < result.length - 1) {
var current = result[i],
next = result[i+1];
// check if there is an overlapping
if(current[1] >= next[0]) {
current[1] = Math.max(current[1], next[1]);
// remove next
result.splice(i+1, 1);
} else {
// move to next
i++;
}
}
return result;
};
This function can be used this way:
var mergedNumbers = merge(numbers);
DEMO

As #Brett said, this might be a better fit for Code Review (just be sure to include your current implementation). If you post there, put a reference to it here somewhere and I'll move my answer.
Assuming that your numbers array is already sorted correctly, this function should do what you want:
function combine(numbers) {
return numbers.reduce(function(combined, next) {
if (!combined.length || combined[combined.length-1][1] < next[0]) combined.push(next);
else {
var prev = combined.pop();
combined.push([prev[0], Math.max(prev[1], next[1])]);
}
return combined;
}, []);
}
var n = [[10, 20], [19, 40], [40, 60], [70, 80], [75, 76]];
var r = combine(n);
document.write('<pre>' + JSON.stringify(r) + '</pre>');
This "reduces" the original array to the new one using the following logic in the reduce function:
If this is the first pass or the last item does not overlap the current item, push the current item on to the combined array.
Otherwise:
pop the last item off the combined array.
push the combination of the last item and the current item on to the combined array.

Simple concise JavaScript solution:
Algo
Sort the intervals by the start index in ascending order.
If the current interval overlap with the previous one, update the previous interval accordingly.
Otherwise, if the current start value > the previous end value), we put the interval in the result.
Implement code
var merge = (intervals) => {
intervals.sort((a, b) => a[0] - b[0]);
const merged = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const [start, end] = intervals[i];
let prev = merged[merged.length - 1];
if (prev[1] >= start) {
prev[1] = Math.max(prev[1], end);
} else merged.push(intervals[i]);
}
return merged;
};
console.log(merge([[10, 20], [19, 40], [40, 60], [70, 80]]));

let arr = [
[1, 3],
[2, 6],
[5, 10],
[15, 18],
[18, 6],
];
const mergeoverlapping = (arr) => {
if (arr.length < 2) return intervals;
arr.sort((a, b) => a[0] - b[0]);
let top = 0;
let down = arr.length - 1;
let left = 0;
let temp = [];
let right = arr[0].length - 1;
let result = [];
while (top + 1 <= down) {
if (arr[top][right] >= arr[top + 1][left]) {
arr[top + 1][left] = arr[top][left];
temp = [arr[top + 1][left], arr[top + 1][right]];
} else {
result.push(temp);
temp = arr[top + 1];
}
top++;
}
result.push(temp);
console.log(result);
};
console.log(mergeoverlapping(arr));

Expanding on accepted solution to provide more readability and a common use-case which is working with sets of integers where we want [[0,21],[22,42]] => [[0,42]]`:
const arr = [[21,21],[-21,1],[21,42],[5,10],[11,21]].sort((a, b) => a[0] - b[0]);
print(merge(arr));
print(merge(arr, true));
function merge(ranges, integers) { // range must be sorted by 1st element
let prev = ranges[0];
const result = [prev];
for (let i = 1; i < ranges.length; i++) {
const next = ranges[i];
if (next[0] > prev[1] + (integers ? 1 : 0)) {
result.push((prev = next));
continue;
}
if (next[1] > prev[1]) prev[1] = next[1];
}
return result;
}
function print(value) {
console.info(JSON.stringify(value))
}
Previous solutions are for closed intervals/ranges (where boundaries are included). This would be the approach for open intervals/ranges (boundaries not included):
const arr = [[21,21],[-21,1],[21,42],[5,10],[11,21]].filter(([a,b]) => a !== b).sort((a, b) => a[0] - b[0]); // 21 is not included
print(merge(arr));
function merge(ranges) { // range must be sorted by 1st element
let prev = ranges[0];
const result = [prev];
for (let i = 1; i < ranges.length; i++) {
const next = ranges[i];
if (next[0] >= prev[1]) { // >= instead of >
result.push((prev = next));
continue;
}
if (next[1] > prev[1]) prev[1] = next[1];
}
return result;
}
function print(value) {
console.info(JSON.stringify(value))
}

Related

Creating an array from another array

I'm learning Javascript and I'm wondering what the most elegant way to convert this: [1,8]
into this:[1,2,3,4,5,6,7,8]?
Thanks a lot!
const argarray = [1, 8]
const countToN = (array) => {
// init results array
let res = []
// start at the first value array[0] go *up to* the second array[1]
for (let i = array[0]; i <= array[1]; i++) {
res.push(i)
}
// return the result
return res
}
console.log(countToN([1, 10]))
This would accommodate what you're trying to do, but it's fairly brittle. You'd have to check that it's an array and that it has only 2 values. If you had other requirements, I could amend this to account for it.
Here's a solution without loops. Note that this only works with positive numbers. It supports arrays of any length, but will always base the result off of the first and last values.
const case1 = [1, 8];
const case2 = [5, 20];
const startToEnd = (array) => {
const last = array[array.length - 1];
const newArray = [...Array(last + 1).keys()];
return newArray.slice(array[0], last + 1);
};
console.log(startToEnd(case1));
console.log(startToEnd(case2));
Here's a solution that works for negative values as well:
const case1 = [-5, 30];
const case2 = [-20, -10];
const case3 = [9, 14];
const startToEndSolid = (array) => {
const length = array[array.length - 1] - array[0] + 1;
if (length < 0) throw new Error('Last value must be greater than the first value.');
return Array.from(Array(length)).map((_, i) => array[0] + i);
};
console.log(startToEndSolid(case1));
console.log(startToEndSolid(case2));
console.log(startToEndSolid(case3));
A simple for loop will do it. Here's an example that has error checking and allows you to range both backwards and forwards (ie [1, 8], and also [1, -8]).
function range(arr) {
// Check if the argument (if there is one) is
// an array, and if it's an array it has a length of
// of two. If not return an error message.
if (!Array.isArray(arr) || arr.length !== 2) {
return 'Not possible';
}
// Deconstruct the first and last elements
// from the array
const [ first, last ] = arr;
// Create a new array to capture the range
const out = [];
// If the last integer is greater than the first
// integer walk the loop forwards
if (last > first) {
for (let i = first; i <= last; i++) {
out.push(i);
}
// Otherwise walk the loop backwards
} else {
for (let i = first; i >= last; i--) {
out.push(i);
}
}
// Finally return the array
return out;
}
console.log(range([1, 8]));
console.log(range('18'));
console.log(range());
console.log(range([1]));
console.log(range([-3, 6]));
console.log(range([9, 16, 23]));
console.log(range([4, -4]));
console.log(range([1, -8, 12]));
console.log(range(null));
console.log(range(undefined));
console.log(range([4, 4]));
Additional documentation
Destructuring assignment
Use Array#map as follows:
const input = [1,8],
output = [...Array(input[1] - input[0] + 1)]
.map((_,i) => input[0] + i);
console.log( output );

Count total of number array with ignoring overlap number [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed last year.
Improve this question
I have this kind of problem and trying to solve it by using Javascript/Go. Given this array of number set, I would like to find the sum of number. The calculation should ignore the overlap and consider to count it as only once.
const nums = [[10, 26], [43, 60], [24,31], [40,50], [13, 19]]
It would be something like following if translated into the picture.
The result should 41
The rules are
Overlap set of number (pink area) should be count once.
Count total sum of green area.
Total for both.
Any help will be appreciated.
Here's an one-liner solution using javascript (Assuming the correct answer is 41 instead of 42).
The idea is to iterate all interval numbers and put them in a single array, then trim all the duplicates using Set. The time complexity is not optimal but it's short enough.
const nums = [[10, 26], [43, 60], [24, 31], [40, 50], [13, 19]];
const total = new Set(nums.reduce((acc, [from, to]) =>
[...acc, ...Array.from({ length: to - from }, (_, i) => i + from)], [])).size;
console.log(total);
Not sure how to do it with go but it's just a proposal.
You could sort the pairs and reduce by checking the second value and then add the deltas for getting the sum.
const
nums = [[10, 26], [43, 60], [24, 31], [40, 50], [13, 19]],
result = nums
.sort((a, b) => a[0] - b[0] || a[1] - b[1])
.reduce((r, [...a]) => {
const last = r[r.length - 1];
if (last && last[1] >= a[0]) last[1] = Math.max(last[1], a[1]);
else r.push(a);
return r;
}, [])
.reduce((s, [l, r]) => s + r - l, 0);
console.log(result)
Here's my version:
const getCoverage = arr => arr
.reduce((results, el) => {
if (!results.length) {
return [el];
}
let running = true, i = 0;
while(running && i < results.length) {
if (el.some(n => n >= results[i][0] && n <= results[i][1])) {
results[i] = [
Math.min(el[0], results[i][0]),
Math.max(el[1], results[i][1])
];
running = false;
}
i++;
}
if (running) {
results.push(el);
}
return results;
}, [])
.reduce((total, el) => el[1] - el[0] + total, 0);
console.log(
getCoverage([[10, 26], [43, 60], [24,31], [40,50], [13, 19]])
);
The first reducer merges overlapping (and adjacent) intervals and the second one adds up the diffs from the resulting merged ones.
You can spread the values of given 2-D array in an array and sort both of them.
Find the sum of the differences between the edge values of non-overlapping arrays and the sum of difference of values within overlapping area and the result will be the difference between the two.
const nums = [[10, 26], [43, 60], [24, 31], [40, 50], [13, 19]]
let arr = []
//spread given 2-D array
nums.forEach(item => arr=[...arr,...item]);
// Sort both arrays
arr.sort();
nums.sort();
let previtem, sum = 0;
let min , max ;
let count = 0;
//Loop to find sum of difference between non-overlapping values
nums.forEach((item,index) => {
if (index === 0) {
min = nums[0][0], max = nums[0][1]
}
else if (!(item[0]>min && item[0]<max) && !(item[1]>min && item[1]<max))
{
flag = 1;
count = count + (item[0] - max);
min = item[0];
max = item[1];
console.log(item,count);
}
else if (item[0]>min && item[0]<max && !(item[1]>min && item[1]<max)) {
max = item[1];
}
})
// loop to find sum of difference of unique values within overlapping area
arr.forEach(item => {
if (previtem && item !== previtem) {
sum=sum+(item-previtem)
}
previtem = item;
})
//Print the result
console.log(sum - count);

Return true if array is in ascending order and return false if it isnt

I want to create a function that accepts an array of numbers as an argument. If the array of numbers is ascending order it should return true, if not it should return false.
arr = [2, 10, 99, 150]
function areOrdered(arr) {
let sortedArray = arr.sort((a, b) => (a - b));
if (arr === sortedArray) {
return true
}
}
You could go through the array and check if every value is bigger then the previous.
Something like this:
const one = [2, 10, 99, 150];
const two = [2, 1, 3, 4];
const isAscending = (arr) => {
for (let i = 0; i < arr.length; i++) {
if (i > 0 && arr[i - 1] > arr[i]) {
return false;
}
}
return true;
}
console.log(one, isAscending(one));
console.log(two, isAscending(two));
You can use .every() to detect ascending order:
const data1 = [2, 10, 99, 150];
const data2 = [10, 2, 99, 150];
const isAscending = arr => arr.every((v, i, a) => (i == 0 || v >= a[i - 1]));
console.log(isAscending(data1));
console.log(isAscending(data2));
You could stringify the array and then compare. Since sort mutates the array, you need to create a new clone using [...arr] and then sort
function areOrdered(arr) {
let sortedArray = [...arr].sort((a, b) => a - b);
return JSON.stringify(arr) === JSON.stringify(sortedArray)
}
console.log(areOrdered([2, 10, 99, 150]))
console.log(areOrdered([2, 100, 99]))
But the best way to check if the array is sorted is to use a simple for and compare with the next index
function areOrdered(arr) {
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
return false;
}
}
return true
}
console.log(areOrdered([2, 10, 99, 150]))
console.log(areOrdered([2, 100, 99]))
I think this will work (we can use .some to check if any two consecutive elements violates the ascending order rule) -:
let checkIfAscendingArr = (arr) => !arr.some((item,i) => {
if(i > 0){
return (arr[i-1] > item)
}
})
This function returns true if there are one or less items in the array ( the array doesn't pass the condition in the for loop )
If there is more than one item in the array it will check the value it is iterating over with the previous one. The function will return false ( thus ending the loop ) if the previous number is higher than the current one.
If all numbers are looped over the array is in ascending order and the function returns true.
function isAscending( array ) {
for( let i = 1; i < array.length; i++ ){
if( array[i-1] > array[i] )
return false;
}
return true;
}
// example tests
let array1 = [1,2,5,5,8]; // true
let array2 = [1,2,7,5,8]; // false
let array3 = [-10]; // true
const result1 = isAscending( array1 );
const result2 = isAscending( array2 );
const result3 = isAscending( array3 );
console.log( result1 );
console.log( result2 );
console.log( result3 );
I think the most concise solution would be with Array.every where you check on the current element and the next one by index. In this scenario if we run out of the index we just compare to x+1.
let isOrdered = arr => arr.every((x,i,a) => x < (a[i+1] || x+1))
console.log(isOrdered([2, 10, 99, 150]))
console.log(isOrdered([21, 10, 99, 150]))
Here is a single liner using Array.reduce. I know - single liners compromise readability. But this one is just for fun.
var isAscending = [1, 2, 3, 4].reduce((p, c, i, a) => {return p === false ? false : c > p ? i === a.length - 1 ? true :c : false}, 0);
console.log(isAscending)

Code to find position of number inside sorted array after selection sort isn't stable?

This code sorts the array after inserting another element and returns the index of the inserted element in the sorted array (the first position or lowest possible index needs to be returned).
CODE:
function getIndexToIns(arr, num) {
// Find my place in this sorted array.
var sortedarr = sort(combinelists(arr, num).sort());
var pos = [];
for (i = 0; i < sortedarr.length; i++) {
if (sortedarr[i] == num) {
pos.push(i);
}
}
return pos[0];
}
function combinelists(arr1, arr2) {
var newarr = [];
newarr.push(arr2);
for (i = 0; i < arr1.length; i++) {
newarr.push(arr1[i]);
}
return newarr;
}
function sort(arr) {
if (arr.length < 2) {
return arr;
} else {
var l = arr.length / 2;
var leftarr = arr.slice(0, l);
var rightarr = arr.slice(l);
return combine(sort(leftarr), sort(rightarr));
}
}
function combine(array, another_array) {
var result = [];
while (array.length && another_array.length) {
if (array[0].age <= another_array[0].age) {
result.push(array.shift());
} else {
result.push(another_array.shift());
}
}
while (array.length)
result.push(array.shift());
while (another_array.length)
result.push(another_array.shift());
return result;
}
console.log(getIndexToIns([2, 20, 10], 19));
console.log(getIndexToIns([2, 5, 10], 15));
But it doesn't seem to work for all inputs:
It works for the following tests:
[10, 20, 30, 40, 50], 30
[40, 60], 50
[2, 20, 10], 19
But it doesn't work for these:
[2, 5, 10], 15
[5, 3, 20, 3], 5
[3, 10, 5], 3
[10, 20, 30, 40, 50], 35
What's broken?
You use Array#sort() without compareFunction, that means you get a result which every element is treated as string and not as number. That results, probably, to the wrong index.
var sortedarr = sort(combinelists(arr,num).sort());
// ^^^^^^
You coud use a callback like
var sortedarr = sort(combinelists(arr,num).sort(function (a, b) { return a - b; }));
for sorting by numbers.

Find Max value of each x value in JavaScript multidimensional array

I want to 'reduce' the array to only max values for each x (or index 0) value in a JavaScript multidimensional array.
My Array is as follows:
var mulitple = [["1/2013", 1],
["1/2013", 5],
["1/2013", 7],
["1/2013", 6],
["1/2013", 5],
["2/2013", 7],
["2/2013", 10],
["2/2013", 10],
["3/2013", 7],
["3/2013", 10],
["3/2013", 10],
["4/2013", 1],
["4/2013", 5],
["4/2013", 7],
["4/2013", 6],
["4/2013", 5],
["5/2013", 7]];
So the final result should be as follows:
[["1/2013", 7],
["2/2013", 10],
["3/2013", 10],
["4/2013", 7],
["5/2013", 7]];
How can I achieve this in JavaScript.
EDIT:
Aww man who voted my question down.
Anyhow, this is what I have come up with.
var max = 0;
var newarray = [];
for (var i = 1; i < mulitple.length; i++) {
if (mulitple[i - 1][0] == mulitple[i][0]) {
if (mulitple[i - 1][1] > max) {
max = mulitple[i - 1][1];
}
}
else {
if (mulitple[i - 1][1] > max) {
max = mulitple[i - 1][1];
}
newarray.push([mulitple[i - 1][0], max]);
max = 0;
}
}
newarray.push([mulitple[mulitple.length - 1][0], max]);
The problem that I am having is that I can't get that last value (for the lone record) to get in the array. This was my result after I ran the code above.
[["1/2013", 7], ["2/2013", 10], ["3/2013", 10], ["4/2013", 7], ["5/2013", 0]]
This assumes that original array is already sorted. If not, you will have to write additional function to sort out.
function findMaximums(data) {
var out = [], maximums = {}, order = new Set;
data.reduce(function(acc, pair) {
if (
// Accumulator has value and it's lower than current
(acc[pair[0]] && acc[pair[0]][1] < pair[1]) ||
// Accumulator doesn't have value
!acc[pair[0]]
) {
acc[pair[0]] = pair; // Store maximum in accumulator
order.add(pair[0]) // Store order in set
}
return acc;
}, maximums);
order.forEach(function(key) {
out.push(maximums[key]); // Populate out with maximums by order
});
return out;
}
findMaximums(multiple);
/*[
[
"1/2013",
7
],
[
"2/2013",
10
],
[
"3/2013",
10
],
[
"4/2013",
7
],
[
"5/2013",
7
]
]*/
Update 1: same, but without Set.
function findMaximums(data) {
var order = [];
var maximums = data.reduce(function(acc, pair) {
if (
// Accumulator has value and it's lower than current
(acc[pair[0]] && acc[pair[0]][2] < pair[1]) ||
// Accumulator doesn't have value
!acc[pair[0]]
) {
// Store maximum
acc[pair[0]] = pair;
// Store order
if (order.indexOf(pair[0]) === -1) {
order.push(pair[0])
}
}
return acc;
}, {});
return order.map(function(key) {
return maximums[key]; // Populate out with maximums by order
});
}
Update 2: Shorter version.
function findMaximums(data) {
return data.filter(function(p1, i1) {
return !data.some(function(p2, i2) {
return p1[0] === p2[0] && ( (p1[1] < p2[1]) || (p1[1] === p2[1] && i1 > i2) );
});
});
}
In this version I let pair to remain in output if there are no other pairs in input data that:
Have same month.
Have bigger value.
or
Have same value, but occur earlier than tested pair. This prevents duplicates.
Read here more about used Array methods: filter, some.
Assuming the array as defined in the original question, which is sorted to have each grouping together...
Completely untested code:
var reduced = [];
var groupName = '';
var groupMax;
var groupIndex;
var l = multiple.length; // Grab the array length only once
for (var i = 0; i < l; i++){
// Current Group Name doesn't match last Group Name
if (multiple[i][0] !== groupName) {
// Last Group Name is not empty (it's not the first Group)
if (groupName !== '') {
// Assume groupIndex has been set and grab the item
reduced.push(multiple[groupIndex]);
}
// Grab the new Group Name and set the initial Max and Index
groupName = multiple[i][0];
groupMax = multiple[i][1];
groupIndex = i;
}
// Current Value is bigger than last captured Group Max
if (multiple[i][1] > groupMax) {
// Grab the new Group Max and the current Index
groupMax = multiple[i][1];
groupIndex = i;
}
}
// Grab the last index, since there's no new group after the last item
reduced.push(multiple[groupIndex]);
There could be some syntax or logic errors. I didn't actually run this code, but I think the concept is correct.
Here's a tested version using a map to collect all the unique values, then the output is sorted by month/year and is independent of the order of the input data. This also works in all browsers (IE6+).
Working demo: http://jsfiddle.net/jfriend00/dk1tc73s/
function findLargest(list) {
var map = {}, output = [], item, key, val, current;
for (var i = 0; i < list.length; i++) {
item = list[i];
key = item[0];
val = item[1];
current = map[key];
if (current) {
// this date is in the map, so make sure to save the largest
// value for this date
if (val > current) {
map[key] = val;
}
} else {
// this date is not yet in the map, so add it
map[key] = val;
}
}
// map contains all the largest values, output it to an array
// the map is not in a guaranteed order
for (var key in map) {
output.push([key, map[key]])
}
// actually parse to numbers in sort so the comparison
// works properly on number strings of different lengths
function parseDate(str) {
var split = str.split("/");
return {m: +split[0], y: +split[1]};
}
// now sort the output
output.sort(function(t1, t2) {
var diffYear, a, b;
a = parseDate(t1[0]);
b = parseDate(t2[0]);
diffYear = a.y - b.y;
if (diffYear !== 0) {
return diffYear;
} else {
return a.m - b.m;
}
});
return output;
}

Categories

Resources