Намерете единичен елемент в масив, без да преминавате през масива

Имам масив с обекти, съхранени в него. Искам да хвана обекта в масива, който има име test-song-poll-02. Знам, че мога да премина през него и да направя условно условие, за да проверя името във всеки обект, но се чудех дали има php функция за масив/обект, която може да върне обекта, където name = 'test-song-poll-03'

Array
(
    [0] => stdClass Object
        (
            [name] => test-song-poll-01
            [description] => test-song-poll-01
            [created_at] => 2014-05-02T23:07:59Z
            [count] => stdClass Object
                (
                    [approved] => 63787
                    [pending] => 341
                    [rejected] => 78962
                    [total] => 143090
                )

            [tpm] => 12
            [approved_tpm] => 3
            [pct] => 4
        )

    [1] => stdClass Object
        (
            [name] => test-song-poll-02
            [description] => test-song-poll-02
            [created_at] => 2014-05-02T23:17:20Z
            [count] => stdClass Object
                (
                    [approved] => 9587
                    [pending] => 0
                    [rejected] => 9780
                    [total] => 19367
                )

            [tpm] => 5
            [approved_tpm] => 3
            [pct] => 1
        )

    [2] => stdClass Object
        (
            [name] => test-song-poll-03
            [description] => test-song-poll-03
            [created_at] => 2014-05-02T23:19:06Z
            [count] => stdClass Object
                (
                    [approved] => 26442
                    [pending] => 0
                    [rejected] => 36242
                    [total] => 62684
                )

            [tpm] => 25
            [approved_tpm] => 9
            [pct] => 2
        )
)

АКТУАЛИЗИРАХ моя код, за да покажа как искам да предам променлива в:

function get_results()
{
    $hashtag = "test-song-poll-03";
    $this->load->model('artist_model');
    $data['results'] = $this->artist_model->get_results();

    $myobject = array_filter($data['results']->streams, function($e, $hashtag) {
      return strcmp($e->name, $hashtag) == 0;
    });

    print_r($myobject);
}

person Brad    schedule 04.05.2014    source източник
comment
Имате ли контрол върху това как се конструира масивът?   -  person vascowhite    schedule 04.05.2014
comment
Не, изтеглям json данни от api.   -  person Brad    schedule 04.05.2014


Отговори (2)


Можете да използвате array_filter.

$myobjects = array_filter($objects, function($e) use($hashtag) {
  return strcmp($e->name, "test-song-poll-03") == 0;
});

Поради анонимната функция, това ще работи само PHP >= 5.3

Можете да използвате собствена функция, ако имате по-стара версия.

person Dávid Szabó    schedule 04.05.2014
comment
Можете ли да направите test-song-poll-03 променлива, която да се предава там? Опитах да го предам като параметър във функцията, където подавате $e, но ми даде грешка: Предупреждение: Липсващ аргумент 2 за Home::{closure}() в и Забележка: Недефинирана променлива: хаштаг в. Променливата, която опитвам за преминаване е $hashtag. Актуализирах кода, за да ви покажа как използвам вашия код. - person Brad; 04.05.2014
comment
Нямам представа за това, но можете да опитате с 'global $hashtag;' може би. Ще го тествам. - person Dávid Szabó; 04.05.2014
comment
Редактирах отговора си, можете да използвате 'use($hashtag)' за предаване на променливи в затварянето. - person Dávid Szabó; 04.05.2014
comment
Този код е леко подвеждащ, той връща масив, а не един обект. Този масив съдържа всички (!) обекти със съответното име, но може да има и нула или няколко такива стойности. Освен това, той не избягва преминаването през масива, което може да бъде скъпо от изчислителна гледна точка. Ако това е важно, имате нужда от масив, който се преобразува от търсеното име към съответния обект (или обекти?), подобно на индекс на база данни. - person Ulrich Eckhardt; 05.05.2014
comment
Той изтегля данните от API, не може да промени масива, така че няма друг начин. - person Dávid Szabó; 05.05.2014
comment
Това ще премине през целия масив, дори ако намери съвпадение в първата итерация? С foreach цикъл можете да се спасите веднага щом намерите съвпадение. Не съм убеден, че това е най-добрият вариант. - person vascowhite; 05.05.2014
comment
Вие сте истински vascowhite, OP не поиска НАЙ-ДОБРАТА опция, той искаше функция в PHP. Ако препрочетеш въпроса му, ще видиш. - person Dávid Szabó; 06.05.2014

Фактът, че получавате данните си от API във формат json, не означава, че трябва да ги съхранявате във формата, който ви е даден. Има няколко стратегии, които ще бъдат по-ефективни от приетия от вас отговор.

Проблемът с текущо избрания отговор е, че той ще обхожда пълния набор от данни всеки път, когато го използвате, дори ако се намери съвпадение при първата итерация. Не знам колко голям е вашият набор от данни, но предполагам, че е значителен, иначе не бихте задали този въпрос.

Също така не знам колко пъти искате да получите достъп до набора от данни, вероятно и вие не искате, но предполагам, че е достатъчно пъти, за да ви накара да се замислите или, отново, няма да задавате този въпрос.

Да предположим, че имате набор от данни, който се състои от 1000 от вашите stdClass обекти и че питате за всеки от тези обекти веднъж, така че имате достъп до него 1000 пъти.

Сега методът 'array_filter()', който ви беше предложен, трябва да има достъп до всички 1000 елемента (това е O(n)) всеки път, което е общо 1 000 000 итерации.

//Access every element once using array_filter()
$objectArray = [];
$objectNames = [];
for($i = 0; $i < 1000; $i ++){
    $objName = 'object_name_' . ($i + 1);
    $objectNames[] = $objName;
    $obj = new stdClass();
    $obj->name = $objName;
    $obj->description = 'test description';
    $obj->accessed = 0;
    $objectArray[] = $obj;
}
$start = microtime(true);
foreach($objectNames as $name){
    $iterations = getObjectWithArray_Filter($name, $objectArray);
}
$end = microtime(true);
$taken = $end - $start;
echo $iterations . " iterations using array_filter() in $taken seconds<br/>\n";

вижте как работи.

Първата алтернатива, която идва на ум, е обикновен стар foreach()loop, това също е O(n), но цикълът може да бъде написан така, че да се спасява веднага щом намери съвпадение. Така че, ако приемем, че имаме достъп до всеки елемент от масива веднъж, ще имаме 500 500 итерации, спестяване от около 50%. Това може или не може да важи в реалния свят, вие ще бъдете най-добрият преценка за това.

//Access every element once using foreach(){}
$objectArray = [];
$objectNames = [];
for($i = 0; $i < 1000; $i ++){
    $objName = 'object_name_' . ($i + 1);
    $objectNames[] = $objName;
    $obj = new stdClass();
    $obj->name = $objName;
    $obj->description = 'test description';
    $obj->accessed = 0;
    $objectArray[] = $obj;
}

$start = microtime(true);
foreach($objectNames as $name){
    $iterations = getObjectWithForeach($name, $objectArray);
}
$end = microtime(true);
$taken = $end - $start;
echo $iterations . " iterations using foreach(){} in $taken seconds<br/>\n";

вижте как работи.

Втората алтернатива, която ми хрумва, е да премина през масива веднъж и да го запиша в асоциативен масив. Първото преминаване ще бъде O(n), 1000 итерации и след това ще можем да осъществим директен достъп до елемента, който искаме, без изобщо да итерираме през масива, което е O(1). Дава ни 2000 итерации за веднъж достъп до всеки елемент от масива.

//Access every element once using Associative array
$objectArray = [];
$objectNames = [];
for($i = 0; $i < 1000; $i ++){
    $objName = 'object_name_' . ($i + 1);
    $objectNames[] = $objName;
    $obj = new stdClass();
    $obj->name = $objName;
    $obj->description = 'test description';
    $obj->accessed = 0;
    $objectArray[] = $obj;
}

$associativeArray = [];
$start = microtime(true);
foreach($objectArray as $object){
    $associativeArray[$object->name] = $object;
    $object->accessed ++;
}

foreach($objectNames as $name){
    $iterations = getObjectFromAssociativeArray($objName, $associativeArray);
}
$end = microtime(true);
$taken = $end - $start;
echo $iterations . " iterations using associative array{} in $taken seconds<br/>\n";

вижте как работи.

Ето останалата част от тестовия ми код: -

//=================================================================
function getObjectWithArray_Filter($objectName, array $objectArray){
    $myobjects = array_filter($objectArray, function($e) use($objectName) {
            $e->accessed ++;
            return strcmp($e->name, $objectName) == 0;
        });
    $iterations = 0;
    foreach($objectArray as $object){
        $iterations += $object->accessed;
    }
    return $iterations;
}

function getObjectWithForeach($objectName, array $objectArray){
    $iterations = 0;
    $found = false;
    $count = 0;
    while(!$found){
        $objectArray[$count]->accessed ++;
        if($objectArray[$count]->name === $objectName){
            $found = true;
        }
        $count ++;
    }
    foreach($objectArray as $object){
        $iterations += $object->accessed;
    }
    return $iterations;
}

function getObjectFromAssociativeArray($objectName, array $objectArray){
    $iterations = 0;
    if($objectName === $objectArray[$objectName]->name){
        $objectArray[$objectName]->accessed ++;
    }
    foreach($objectArray as $object){
        $iterations += $object->accessed;
    }
    return $iterations;
}

tl;dr
Изход на 3v4l.org:-

Accessing 1000 elements once took 1000000 iterations using array_filter() in 0.5374710559845 seconds
Accessing 1000 elements once took 500500 iterations using foreach(){} in 0.2077169418335 seconds
Accessing 1000 elements once took 2000 iterations using associative array{} in 0.1438410282135 seconds

вижте как работи.

Разликите във времето също са интересни. Може или не може да се наложи да оптимизирате за скорост като тази, но бих предложил, че това е полезна промяна, която да направите във вашия код. Във всеки случай бих си помислил, че липсата на итерации след първата е много по-добра от 1000 или средно 500,5 всеки път.

Надявам се, че виждате това като полезно упражнение, вашият въпрос събуди интереса ми и бях убеден, че отговорът, който приехте, не беше най-доброто решение за вас. Все още може да мислите, че е така, но аз предлагам това като алтернатива.

Най-простият начин за прилагане на това би бил да се използва някакъв вид хранилище/фабрика на обекти: -

class ObjectStore
{
    private $decoded;
    private $asssocArray;

    public function __construct($jsonEncodedObjects)
    {
        $this->decoded = json_decode($jsonEncodedObjects);
    }

    public function getObject($objectName)
    {
        if(!$this->asssocArray){
            foreach($this->decoded as $object){
                $this->asssocArray[$object->name] = $object;
            }
        }
        return $this->asssocArray[$objectName];
    }
}

По този начин първата ви заявка за обект е O(n), а следващите заявки ще бъдат O(1).

За да използвате това с вашия код:-

$objectStore = new ObjectStore(getJsonEncodedData());

$hashtag = "test-song-poll-03";
$myObject = $objectStore->getObject($hashtag);
person vascowhite    schedule 05.05.2014
comment
Вярно си, OP не поиска НАЙ-ДОБРАТА опция, той искаше функция в PHP. Ако препрочетеш въпроса му, ще видиш. - person Dávid Szabó; 06.05.2014
comment
@newboyhun Това, което хората искат, не винаги е това, от което се нуждаят, вашето решение не е лошо, просто не е подходящо за този случай. Това причинява на OP допълнителни проблеми. - person vascowhite; 06.05.2014