Filter 3-level array using an associative array defining the numeric values which must be exceeded

478 views Asked by At

I have an associative array with skill ids and its eligible marks/scores. E.g.:

Array
(
    [3] => 2 // skill => eligible marks
    [63] => 6
    [128] => 3
)

And I have a multidimensional array with student ids as first level keys. The second level contains indexed subarrays representing skill ids and marks/scores as 2-element associative arrays.

Array
(
    [22] => Array
        (
            [0] => Array
                (
                    [skill_id] => 3
                    [gd_score] => 4
                )

            [1] => Array
                (
                    [skill_id] => 128
                    [gd_score] => 6
                )

        )

    [23] => Array
        (
            [0] => Array
                (
                    [skill_id] => 128
                    [gd_score] => 3
                )

        )

    [24] => Array
        (
            [0] => Array
                (
                    [skill_id] => 3
                    [gd_score] => 7
                )

            [1] => Array
                (
                    [skill_id] => 63
                    [gd_score] => 8
                )

            [2] => Array
                (
                    [skill_id] => 128
                    [gd_score] => 9
                )

        )

)

I want to filter the students based on the values in the first array.

I want to get all students with:

  • skill 3 marks greater than 2 and
  • skill 63 marks greater than 6 and
  • skill 128 marks greater than 3.

If all criteria are satisfied, return the student id. Because only student 24 meets all requirements, the output should be [24] -- an array with a single element.

2

There are 2 answers

0
RomanPerekhrest On BEST ANSWER

Use the following approach:

$marks = array
(
    3 => 2, // skill => eligible marks
    63 => 6,
    128 => 3
);

// $arr is your initial array of student data
$student_ids = [];
$marks_count = count($marks);
foreach ($arr as $k => $items) {
    // if number of marks coincide
    if ($marks_count != count($items)) {
        continue;
    }
    
    foreach ($items as $item) {
        if (!isset($marks[$item['skill_id']]) 
            || $marks[$item['skill_id']] >= $item['gd_score']
        ) {
            continue 2;
        }
    }
    $student_ids[] = $k;
}

print_r($student_ids);

The output:

Array
(
    [0] => 24
)

Test link: https://eval.in/private/10a7add53b1378

0
mickmackusa On

Credit to @RomanPerekhrest for an efficient nested loop approach with a conditional early continues. I do think that there are some scenarios where assumptions about data quality would cause trouble, but I will refrain from inventing code-fouling test cases.

Anyhow, I don't think I've ever needed to experiment with array_udiff_assoc() before, so this was a good opportunity.

My snippet will associatively filter out all first level entries where required tests are missing or the actual scores are NOT greater than the qualifying scores.

My snippet is not designed to outperform Roman's nested loops and I didn't benchmark it. I just wanted to offer a concise, functional-style approach.

Sample Data:

$criteria = [3 => 2, 63 => 6, 128 => 3];

$allScores = [
    22 => [
        ['skill_id' => 3, 'gd_score' => 4],
        ['skill_id' => 999, 'gd_score' => 9],
        ['skill_id' => 128, 'gd_score' => 7],
    ],
    23 => [
        ['skill_id' => 128, 'gd_score' => 3],
    ],
    24 => [
        ['skill_id' => 63, 'gd_score' => 8],
        ['skill_id' => 3, 'gd_score' => 7],
        ['skill_id' => 128, 'gd_score' => 9],
    ],
    25 => [
        ['skill_id' => 3, 'gd_score' => 7],
        ['skill_id' => 63, 'gd_score' => 8],
        ['skill_id' => 128, 'gd_score' => 1],
    ],
    26 => [
        ['skill_id' => 3, 'gd_score' => 2],
        ['skill_id' => 63, 'gd_score' => 6],
        ['skill_id' => 128, 'gd_score' => 3],
    ],
];

Code: (Demo)

var_export(
    array_keys(
        array_filter(
            $allScores,
            fn($scores) => !array_udiff_assoc(
                $criteria,
                array_column($scores, 'gd_score', 'skill_id'),
                fn($cVal, $sVal) => $cVal >= $sVal
            )
        )
    )
);

Output:

array (
  0 => 24,
)

If I was going to craft a nested loop script, (assuming the skill scores cannot be negative), I'd build it like this: (Demo)

$result = [];
foreach ($allScores as $key => $scores) {
    $skillScores = array_column($scores, 'gd_score', 'skill_id');
    foreach ($criteria as $id => $toBeat) {
        if (($skillScores[$id] ?? 0) <= $toBeat) {
            continue 2;
        }
    }
    $result[] = $key;
}
var_export($result);

Like the earlier functional snippet, this snippet, provides the same output and allows optional skills data to exist without breaking the business rules. Creating a lookup array with array_column() avoids the need to loop through the subarray of scores for each rule.

The second snippet is likely to outperform the first, but if it does I don't expect the difference to be very noticable