Ranges revisited

So I have this range reduce call that was posted here on the forum


function reduce_ranges($reduced, $value) {
    static $in_range, $previous;
    // Reset
    if ($in_range === NULL) {
        $in_range = FALSE;
        $previous = NULL;
    }
    // In a range
    if (isset($previous) && $value == $previous + 1) {
        if (!$in_range) {
            $in_range = TRUE;
        }
        // Update the range
        end($reduced);
        $key = key($reduced);
        $start = strtok($reduced[$key], "-");
        $reduced[$key] = $start."-".$value;
    // Not in a range
    } else {
        $reduced[] = $value;
        $in_range = FALSE;
    }
    $previous = $value;
    return $reduced;
}

Works fine, until alphanumeric codes are used. Starting this year, the State of Tennessee decided to do just that with license plate decals, so I have to rewrite this method to tolerate numbers with alpha prefixes.

For example, finding a range of: A100-A200

However if the letter does change it needs to start a new range (obviously).

I’m guessing I’ll need to use a regex on these, and that’s one area of code I’m not strong with. If it helps, for now, if there is a letter present it will be the first character of the string.

Which part in particular, if any, are you having trouble with? I don’t know about other folks here, but I’d rather help you write your code than cook up some “solution” that you might not want.

Hi Michael,

I searched the PHP forums in hope of seeing the input for this function so I could better understand what data should output, but could not find its’ original reference. Do you mind showing a small example in the context that it is used?

Steve

For those that are not familiar with the ranges in the States, can you supply all the other ranges.

Steve, it’s a callback for array_reduce(). See http://www.sitepoint.com/forums/showthread.php?665995-how-to-add-commas-and-dashes-for-numbers-ie-1-10-or-1-4-13&p=4548334&viewfull=1#post4548334

Thanks Salathe… saves Michael from redoing this and examples it well!

Steve

Up until this year Decal numbers of the state of Tennessee where simply numeric. As of this year the “numbers” may have a letter - as in “A002841200”. I need to ignore that letter for constructing ranges, but if the letter changes, I need to respect that and start a new range. There is NOT a foldover from A9999 to B0000, those will be the ends of two different ranges. I should never have a range A234 - B100 or the like.

Hi Michael,

I don’t know if this will help but I created this function that will split the alphanumeric string into the Letter and the number; it returns an array(‘number’=>10000, ‘string’=>‘A’);

Here is the function:


function split_alphanumeric($string){
  preg_match_all('/([\\d]+)/',$string, $num_target); // matches just the numbers   
  preg_match_all('/[a-zA-Z]/', $string, $str_target); // matches just the alpha character(s)
  $num = null;
  $num = $num_target[0][0];
  $str = null;
  $str = $str_target[0][0];
  $values = array();
  $values['number'] = (int)$num;
  $values['string'] = $str;
  return $values;
}

And using it:


$string = 'A10000';
$values = split_alphanumeric($string);
echo '<pre>';var_dump($values); echo "</pre>";

Creates this:


[COLOR=#000000]array(2) {[/COLOR]["number"]=>  int(10000)  ["string"]=>  string(1) "A" [COLOR=#000000]}[/COLOR]

It would always create an array for the start of the range and would need to be recalled for the ending of the range. Alternatively the regex could be embedded in your function.

Steve

My solution. Hamfisted probably, but it is working


function reduce_ranges($reduced, $value) {
	
    static $in_range, $previous, $prefix;
    // Reset
    if ($in_range === NULL) {
        $in_range = FALSE;
        $previous = NULL;
        $prefix = NULL;
    }

    // Using the newer prefixed numbers system.
    if (!is_numeric($value)) {
    	$currentPrefix = substr($value, 0, 1);
    	$numericPiece = (int) substr($value, 1, strlen($value));
    } else {
    	$numericPiece = $value;
    }
 
    // In a range
    if (isset($previous) && $numericPiece == $previous + 1 && $currentPrefix == $prefix ) {
        if (!$in_range) {
            $in_range = TRUE;
        }
        // Update the range
        end($reduced);
        $key = key($reduced);
        $start = strtok($reduced[$key], "-");
        $reduced[$key] = $start."-".$value;
    // Not in a range
    } else {
        $reduced[] = $value;
        $in_range = FALSE;
        $prefix = $currentPrefix;
    }
    $previous = $numericPiece;
    return $reduced;
} 

Well, it was working in test. I spent half of the day double guessing this function - turns out it busts if there are duplicate entries in the range. For my particular application that’s bad - that means the prior coder did not check his inputs when he wrote that code awhile ago.

Glad you got it working Michael. I knew having sit in this forum as long as it did that you would most likely have found a way.

Thanks for sharing your solution.

Too bad the previous coder did not validate the input… this is an important lesson for every php coder :slight_smile:

Steve

That’s usually a sign to step back and look at the problem (and “solution”) again. The reducing function from the first post was written for a (slightly) different case, for a user who appeared to just want some code—any code!—that worked and run away with it, and was written by someone who uses array_reduce() and friends very regularly.

It deliberately wasn’t the simplest approach nor did it pass the what-on-earth-does-this-do-at-a-glance test. I also tend to forget that others may be choosing to pick up code posted on the forums, expecting them to work for them too!

Yep, it doesn’t particularly like duplicates. (:

[HR][/HR]

If you’re not bent on going to the trouble of using array_reduce() and a callback function, a more basic approach might be employed: a single loop over the array, building up the ranges step by step.


function get_ranges($array) {
    $reduced  = array();
    $expected = NULL;
    $key      = 0;
    foreach ($array as $current) {
        if ($current === $expected) {
            // Got expected next value in range
            // Update current range to show new end value
            $start = strtok($reduced[$key], '-');
            $reduced[$key] = $start.'-'.$current;
        } else {
            // Broken sequence, start a new range
            $reduced[++$key] = $current;
        }
        // Calculate what the next value in the range should be
        if (ctype_digit($current)) {
            $expected = (string) ($current + 1);
        } else {
            $expected = $current[0] . (substr($current, 1) + 1);
        }
    }
    return $reduced;
}

Hope that makes sense. Oh and the function above will also barf on duplicates, but it’s only a couple of lines extra in the right place if you still need to deal with them inside the function. I’m happy to help if that is the case.

The thing is, the solution works. Duplicates are forbidden in this data structure - so that’s an error in the code I wasn’t aware of created by another programmer that I now have to fix.

I don’t know what you want. A lollipop?