17

I have an array in the following format:

array(
  0 => array(1, 5),
  1 => array(4, 8),
  2 => array(19, 24),
  3 => array(6, 9),
  4 => array(11, 17),
);

Where each item is a X-to-Y range. What I would like to merge the overlapping ranges in the array, to get something more like this:

array(
  0 => array(1, 9), // 1-5, 4-8 and 6-9 are overlapping, so they are merged
  1 => array(11, 17),
  2 => array(19, 24),
);

What would be the best way to accomplish this?

3 Answers 3

23

Untested, but the idea here is to sort the data first by the first element, then merge subsequent elements with the previous one as long as possible.

usort($data, function($a, $b)
{
        return $a[0] - $b[0];
});

$n = 0; $len = count($data);
for ($i = 1; $i < $len; ++$i)
{
        if ($data[$i][0] > $data[$n][1] + 1)
                $n = $i;
        else
        {
                if ($data[$n][1] < $data[$i][1])
                        $data[$n][1] = $data[$i][1];
                unset($data[$i]);
        }
}

$data = array_values($data);
Sign up to request clarification or add additional context in comments.

6 Comments

+1 This is the cleanest and most efficient being O(n). This is exactly the algorithm I had in mind, you beat me to it.
What does the +1 serve for @ $data[$n][1] ? When using floating point numbers this does not work in my case.
@Tom, with integers, you want [1,2],[3,4] to be one single range of [1,4]. In that case, it would read if (3 > 2 + 1) then start a new range. With floating point numbers, it's not really useful. The +1, can be dropped or set to a very small delta (+.00001), depending on what you consider small enough to be the same number.
How is this O(n)? The sort in the beginning is nlogn already.
You are an evil genius!! You saved me some CPU and few lines of code although I'm doing this on dates (just had to use strtotime() everywhere except second if() block (obviously!) and add 60 (i.e 60 sec) instead of 1 in first if() block).. would love to see(as update) if there is a better approach for merging datetime ranges (not desperate enough to post a new question though).
|
1
$input = array( 0 => array(1, 5),
                1 => array(4, 8),
                2 => array(19, 24),
                3 => array(6, 9),
                4 => array(11, 17),
              );


$tmpArray = array();
foreach($input as $rangeSet) {
    $tmpArray = array_unique(array_merge($tmpArray,range($rangeSet[0],$rangeSet[1])));
}


sort($tmpArray);

$oldElement = array_shift($tmpArray);
$newArray = array(array($oldElement));
$ni = 0;
foreach($tmpArray as $newElement) {
    if ($newElement > $oldElement+1) {
        $newArray[$ni++][] = $oldElement;
        $newArray[$ni][] = $newElement;
    }
    $oldElement = $newElement;
}
$newArray[$ni++][] = $oldElement;

var_dump($newArray);

1 Comment

This method should work, but it will slow down to a crawl with large ranges.
0

Alright, drafted this up, so it may have quirks. Tested it with the data seen below and seemed to work just fine. May not be the best way to do it, but it is one way and it does work. Questions let me know.

function combineRange($array) {
    if (is_array($array)) {
        // Sort the array for numerical order
        sort($array);

        // Set Defaults
        $prev = array();
        $prev_key = null;

        foreach ($array as $key => $item) {
            // First time around setup default data
            if (empty($prev)) {
                $prev = $item;
                $prev_key = $key;
                continue;
            }

            if ($item[0] >= $prev[0] && $item[0] <= $prev[1]) {
                // Incase the last number was less than do not update
                if ($array[$prev_key][1] < $item[1])
                    $array[$prev_key][1] = $item[1];

                unset($array[$key]);
            }else {
                $prev_key = $key;
            }       

            $prev = $item;
        }
    }

    return $array;
}

$array = array(
  5 => array(13, 16),
  0 => array(1, 5),
  1 => array(4, 8),
  2 => array(19, 24),
  3 => array(6, 9),
  4 => array(11, 17),
  6 => array(21, 30),
);

var_dump(combineRange($array));

Outputs:

array(3) {
  [0]=>
  array(2) {
    [0]=>
    int(1)
    [1]=>
    int(9)
  }
  [3]=>
  array(2) {
    [0]=>
    int(11)
    [1]=>
    int(17)
  }
  [5]=>
  array(2) {
    [0]=>
    int(19)
    [1]=>
    int(30)
  }
}

Hope it works for ya!

EDIT

I see I was beaten out by an hour =\ Oh well! I am still posting as it is a different method, granted I would probably choose konforce's method instead.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.