ravasaurio's avatar

Deciding what item remains in a collection after unique method is applied

I have a test model and a question model, with a many to many relationship. I am making a method that combines n tests. It takes all the questions in all the tests and creates a new test using the questions on the tests, removing duplicates.

$questions = $questions->unique('id')->sortBy('id');

But I can not simply use the ID. See, each question has a tag field, which I am using for localization purposes (@lang(questions.the_tag_here)). This field is unique for almost every question. But, there are some questions that share the exact same text, so they are using the exact same tag. The text may be the same, but for there are key differences in other fields. There can not be more than one of these questions in the same test, so in the event of combining two or more tests, if these questions are present, I need to take only the one with the longest string in one of the other fields.

If I simply do this:

$questions = $questions->unique('tag')->sortBy('id');

It will choose to keep the first question on the database.

How can I choose the one with the longest string on that other field instead?

Edit:

Using Laravel's documentation as example:

$collection = collect([
    ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'],
    ['name' => 'iPhone 5', 'brand' => 'Apple', 'type' => 'phone'],
    ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'],
    ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'],
    ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'],
]);

$unique = $collection->unique('brand');

$unique->values()->all();

/*
    [
        ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'],
        ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'],
    ]
*/

What if I want the iPhone 5 instead of the 6?

P.D: I hope I explained myself well, english is not my first language.

0 likes
9 replies
munazzil's avatar

Can you display your model here because you have to add in model as like below.

    protected $primaryKey = 'id';
shushkin's avatar

You may also pass your own callback to determine item uniqueness:

$unique = $collection->unique(function ($item) {
    return $item['brand'].$item['type'];
});

$unique->values()->all();

/*
    [
        ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'],
        ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'],
        ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'],
        ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'],
    ]
*/
ravasaurio's avatar

@SHUSHKIN - I have been trying to use that but I don't know how to.

$questions = $questions->unique(function ($item) {
         //What here?
       })->sortBy('id');

The problem is: I have n questions in the collection that share the same tag. I need to keep only one of those, but I need exactly the one with the longest string in another field.

munazzil's avatar

@ravasaurio

Do you check with what i have said to you. Can you show your Question and Collection model over here?

shushkin's avatar
shushkin
Best Answer
Level 5

i guess first you need make sorting

$questions = $questions->sortByDesc(function ($item) {
    return strlen($item->YOUR PROPERTY);
});

and then

$questions = $questions->unique('tag')

or shorter

$questions = $questions->sortByDesc(function ($item) {
    return strlen($item->YOUR PROPERTY);
})->unique('tag');
D9705996's avatar

Why don't you create a custom accessor on you question model that ensures the uniqueness then use this with the unique function

    public function getKeyAttribute($value)
    {
        return $value; // replace with you logic
    }

You will need to add protected $appends = ['key'] to your question model

You can then do

$collection->unique('key');

FYI - I would pick a better name than key

ravasaurio's avatar

@SHUSHKIN - Sorry I had to wait the whole weekend to test this, and it works just fine! Thank you very much.

shushkin's avatar

@ravasaurio but i am not sure that solution is the best practice

maybe there exists more elegant solution

pilat's avatar

with unique(), the first item it stumbled upon will remain. BUT, this is not common for all the methods. In keyBy(), for example, the last item will replace anything before

$coll = [
    ['tag' => 1, 'value' => 11],
    ['tag' => 1, 'value' => 12],
    ['tag' => 2, 'value' => 21],
    ['tag' => 2, 'value' => 22],
];

[
    'unique' => collect($coll)->unique('tag')->pluck('value')->all(),
    // BUT!
    'keyBy' => collect($coll)->keyBy('tag')->pluck('value')->all(),
];

// output:
[
    "unique" => [11, 21],
    "keyBy" => [12, 22],
  ]

Please or to participate in this conversation.