Get offset neighbors of an array

2020-07-28 11:55发布

问题:

TL;DR

How can I slice an array to get a (user-defined) number of entries before and after a given offset (also user-defined) without go out of range. E.g:

$collection = [ 'A', 'B', 'C', 'D', 'E' ];

If defined a limit of 2 and a starting offset of index 3 (D), the routine should return B, C, D, E: Two before D and just one after D, otherwise it would go out of range.

If defined a limit of 3 and a starting offset of index 0 (A), the routine should return A, B, C, D, three after A and none before it, otherwise it would also go out of range.

The slice must expand to always bring the number of elements defined by $limit before and after the entry found at $offset. Unless one of the sides goes out of range, of course


I'm trying to figure a simple, perhaps mathematical only way, to get the offset neighbors of an indexed (and currently sequential) array, but with a custom limit.

First thing that came to me was to use array_slice():

$collection = range( 'A', 'Z' );
$offset     = 0; //rand( 0, 25 );
$limit      = 3;

$length = count( $collection );

if( $offset >= $length ) {
    throw new \OutOfRangeException( 'Requested offset exceeds the size of Collection' );
}

$start = ( $offset - $limit ) >= 0 ? ( $offset - $limit ) : 0;
$end   = $limit + 2;

$slice = array_slice( $collection, $start, $end );

From what I could test, $start is working fine. I'm forcing the first offset to avoid a negative one.

Then I first thought I could just increase $limit by 2 to have the "second offset", and of course it didn't work, but that was my very fault. I don't use that function so often >.<

Then I changed the logic of $end to:

$end = $offset + $limit + 1;

The offset I've found in the collection, plus the limit and one more to include the very entry stored in $offset. It worked fine until $offset was set to 4 (E) or more. After that, for the letter 'F' the slice was going up to G, H up to I and so on o.O

I tried to search for a solution but what I could find so far were involving loops and complex conditions. As far as I can tell I shouldn't need that, after all if it's sequential, I could apply "simple" math over it.

But mine didn't work so I may be wrong...

回答1:

This is your cleanest approach -- no conditional calculations required.

Code: (Demo)

$collection = range( 'A', 'Z' );
$offset     = 1; //rand( 0, 25 );
$limit      = 3;
$indices=array_flip(range($offset-$limit,$offset+$limit));  // genereate offset range and flip values to keys
//var_export($indices);
/*array (
  -2 => 0,
  -1 => 1,
  0 => 2,
  1 => 3,
  2 => 4,
  3 => 5,
  4 => 6,
)*/
var_export(array_intersect_key($collection,$indices));  // retain values with listed keys

Output:

array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'E',
)

I can confirm that array_fill_keys() works just as well in place of array_fill(). Here is another demo:

Code: (Demo)

$collection = range('A', 'Z');
for ($i = 0; $i < 5; ++$i) {
    $offset = rand(0, 25);
    $limit = rand(1, 5);
    echo "Offset: $offset\nLimit: $limit\n";
    var_export(array_intersect_key($collection, array_fill_keys(range($offset-$limit, $offset+$limit), null)));
    echo "\n---\n";
}

Output:

Offset: 13
Limit: 3
array (
  10 => 'K',
  11 => 'L',
  12 => 'M',
  13 => 'N',
  14 => 'O',
  15 => 'P',
  16 => 'Q',
)
---
Offset: 0
Limit: 5
array (
  0 => 'A',
  1 => 'B',
  2 => 'C',
  3 => 'D',
  4 => 'E',
  5 => 'F',
)
---
Offset: 12
Limit: 2
array (
  10 => 'K',
  11 => 'L',
  12 => 'M',
  13 => 'N',
  14 => 'O',
)
---
Offset: 25
Limit: 3
array (
  22 => 'W',
  23 => 'X',
  24 => 'Y',
  25 => 'Z',
)
---
Offset: 11
Limit: 5
array (
  6 => 'G',
  7 => 'H',
  8 => 'I',
  9 => 'J',
  10 => 'K',
  11 => 'L',
  12 => 'M',
  13 => 'N',
  14 => 'O',
  15 => 'P',
  16 => 'Q',
)
---


回答2:

Edit again.
This is probably as light as you can possibly make this function.
It compares the start and end values with max and min to make sure it doesn't overflow/underflow.
The to output it uses a substr().
But you can also loop it or explode it. See link for that code.
https://3v4l.org/sfCKY

$alpha  = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$offset = 1;
$limit  = 6;

$start = max(0, $offset-$limit); //if offset-limit is negative value max() makes it 0
$end = min(strlen($alpha)-1, $offset+$limit);  // if offset+limit is greater than str lenght min will make it str lenght.

Echo substr($alpha, $start, $end-$start+1);



Edit I think I found the best solution. Preg_match.

$collection = range( 'A', 'Z' );
$offset     = 1;
$limit      = 6;

$alpha = implode("", $collection);

Preg_match("/.{0," . $limit ."}" . chr(65 +$offset) . ".{0," . $limit ."}/", $alpha, $match);
Var_dump($match);

Sorry for not including an explanation.
I build a regex pattern looking like .{0,6}B.{0,6} but 6 and B is variable from the inputs. This means:
.{0,6} - match anything between zero and six times.
B - match a "B", literally.
.{0,6} - match anything between zero and six times again.

In the regex pattern I use chr() to convert a number ($offset) to a capital letter.
In this case offset is 1 so that means the second letter in alphabet (A is 0).
The $limit is used to, well limit the regex search. {0, <$limit>} this means match as many as you can between 0 and $limit.

https://3v4l.org/BVdiF

No calculations or array functions at all. Just a regex.


Not sure if I got it correct this time.
But calculate the start and end values of array slice depending on if you overflow or underflow on the offset limit.

$collection = [ 'A', 'B', 'C', 'D', 'E' ];
$offset     = 4;
$limit      = 2;

If($offset>count($collection) || $offset < 0 || $limit ==0) die("inputs out of bounds");


If($offset-$limit >=0){
     $start = $offset-$limit;
}else{
     $start =0;
}

If($offset+$limit>count($collection)){
    $end = count($collection)-$offset+$limit;
}else{
    $end = $offset + $limit;
}
If($start ==0) $end++;

$result = array_slice($collection, $start, $end-$start+1);
Var_dump($result);

https://3v4l.org/0l1J6

Edit found some issues.
If start is 0 I needed to add 1, not otherwise.
End limit of max was not working as it should, thanks for pointing that out.
If limit is 0 error message.

I think I got all variations working now.



回答3:

Explanations here : https://stackoverflow.com/a/45532145/1636522.

By the way, I beat them all : https://3v4l.org/5Gt7B/perf#output :-P

PHP version :

$padding = 2;
$letters = range('A', 'Z');
$indexes = ['-1', ' 0', ' 1', ' 2', '13', '24', '25', '26'];
foreach ($indexes as $index) {
  $i = intval($index);
  echo $index . ' ' . (
    isset($letters[$i]) ? $letters[$i] : '/'
  ) . ' [' . implode(', ', array_slice(
    $letters, 
    $i - min($i, $padding), 
    $padding + 1 + min($i, $padding)
  )) . ']' . "\n";
}

JS version :

padding = 2;
letters = new Array(27).join(" ").split("");
letters = letters.map((x, i) => (i + 10).toString(36));
indexes = ["-1", " 0", " 1", " 2", "13", "24", "25", "26"];
for (i = 0; i < indexes.length; i++) {
  index = parseInt(indexes[i], 10);
  s = letters.slice(
    index - Math.min(index, padding), 
    padding + index + 1
  );
  console.log(
    indexes[i], 
    letters[index] || "/", 
    "[" + s.join(", ") + "]"
  );
}