Софтуерно Инженерство
Loading...
+ Нов въпрос
VladoGenov avatar VladoGenov 44 Точки

[PHP Basics - Exercises] - 09. Object to JSON String

Задача: [PHP Basics - Exercises] - 09. Object to JSON String
Линк в Judge: https://judge.softuni.bg/Contests/Practice/Index/240#8
Моят код: http://pastebin.com/9xVcUfxw
Проблем: Използвам $inputLines = array_map('trim',$inputLines); уж се ползва 'trim', но накрая при преобразуване в JSON се появява един празен ред ("":null ), т.е. {"name":"Angel","surname":"Georgiev","age":20,"grade":6,"date":"23/05/1995","town":"Sofia","":null}

 ВЪПРОС: Защо се получава този проблем и какъв е начинът да се избегне?

Тагове:
0
Софтуерни технологии 03/08/2016 22:55:49
RoYaL avatar RoYaL SoftUni Team Trainer 6796 Точки
Best Answer
Да, точно това ми е нужно.
Някаква идея или насока, има ли някаква готова функция или по-скоро последователни операции да ползвам за премахване на празния елемент?

Добре. Нека помислим логично и да спрем да търсим готови функции. Както казваш, последователност от операции. Това, което ни трябва е от масив с различни по стойност елементи, да вземем тези, чиято стойност отговаря на някакво условие и да пропуснем останалите. В конкретния случай искаме стойността да не е празна.

Нека вземем за по-прост пример номериран масив, а не асоциативен:

$arr = ["pesho", "    ",  "gosho", "", "  ", "petko", "mariq", ""];

Това, което искаме да направим е масив, в който присъстват само стойностите "pesho", "gosho", "petko" и "mariq", тъй като са единствените, които отговарят на условието да не са празни.

Да видим първо как проверяваме дали стринг е празен. Варианта, който се сещам на прима виста е да премахнем празните места (спейсове, табулации, нови редове и т.н.) от стринга чрез trim() и след това да видим дали empty() връща true. Ако връща true, значи е празен.

В такъв случай, ако имаме

$str = "   ";

И пробваме

if (empty(trim($str)) {
   echo "празен е";
}

Най-вероятно ще влезем в условието и ще изпечатаме "празен е". Нека тогава си представим, че $str е променлива, която репрезентира всеки елемент от масива. Какво трябва да направим? Същата проверка върху всеки един елемент от масива и ако се окаже, че е празен, го пропускаме. Ако се окаже, че не е празен - нека го напълним в един нов масив. Така, ще резултираме с един нов масив, напълнен само със стойностите, които не са празни.

Как можем да минем през всички елементи на масива? Отговорът е с цикъл. Може с кой да е от познатите ни цикли. Възможно е да се пусне while() цикъл докато има следващи елементи в итератора. Можем да пуснем и for() цикъл до count() от масива. Най-натурално за обхождане на масив се ползва foreach()

$resultArr = [];

foreach ($arr as $value) {
    if (empty(trim($value))) {
        // не прави нищо
    } else {
        $resultArr[] = $value; // добави елемента в новия масив
    }
}

Или опростено, можем просто да кажем - ако НЕ си празен и така да си спестим излишния if, а останем де факто само с else-a:

$resultArr = [];

foreach ($arr as $value) {
    if (!empty(trim($value))) {
        $resultArr[] = $value;
    }
}

И истината е, че постигнахме желаният резултат.

 

Нека сега се върнем към вградените функции, за да видим какво правят те. Ще правим аналогия и с другите преподавани езици C# и/или Java.

Функцията map() изпълнява изпратена от програмиста функция за всеки елемент от масива и резултата го запаметява на мястото на стария елемент. Т.е. тази функция не променя броят елементи в масива, а само тяхната стойност. Еквивалентът на тази функция в C# е Select(), а в Java отново е map();

Нека си представим, че имаме именованата функция "takeSecondChar", която приема аргумент стринг и връща вторият символ от стринга. Искаме да изпълним тази функция за всеки елемент от масива

function takeSecondChar($string) {
    return $string[1];
}

Подобно на твоят запис с "trim", правим следното нещо:

$arr = ["pesho", "    ",  "gosho", "", "  ", "petko", "mariq", ""];

$resultArr = array_map('takeSecondChar', $arr);

Това отдолу ще направи направи цикъл и за всеки елемент от масива ще изпълни функцията и резултата ще го запише на същия индекс, на който е бил старият елемент в нов масив (или същия, ако подадеш същия). Нещо от сорта на:

for ($i = 0; $i < count($arr); $i++) {
    $currentElement = $arr[$i];
    $modifiedElement = takeSecondChar($currentElement);
    $newArr[$i] = $modifiedElement;
}

Като на ред 2 на цикъла се изпълнява тази функция, която си подал ти. В случая takeSecondChar. Какъв е резултатът накрая?

array(8) {
  [0]=>
  string(1) "e"
  [1]=>
  string(1) " "
  [2]=>
  string(1) "o"
  [3]=>
  string(0) ""
  [4]=>
  string(1) " "
  [5]=>
  string(1) "e"
  [6]=>
  string(1) "a"
  [7]=>
  string(0) ""
}

Както виждаме, елементите отново са 8, просто са с нови стойности. Такива, каквито е върнала "takeSecondChar". Същото е и с trim(). Резултатът от trim() ще застане там.

Еквивалентът на това в C# е:

        static char TakeSecondChar(string str)
        {
            return str[1];
        }

        static void Main(string[] args)
        {
            string[] arr = {"pesho", "    ", "gosho", "  ", "   ", "petko", "mariq", "    "};
            char[] newArr = arr.Select(TakeSecondChar).ToArray();
        }

Разбира се, както в C# и Java, така и в PHP, тази функция работи и с анонимни имплементации, не е нужно да са предварително дефинирани.

        static void Main(string[] args)
        {
            string[] arr = {"pesho", "    ", "gosho", "  ", "   ", "petko", "mariq", "    "};
            char[] newArr = arr.Select(arrayElement => arrayElement[1]).ToArray();
        }

Какво казваме тук? "Хей, Select, ето ти функция, която приема един аргумент arrayElement, който е всеки елемент от масива. Нека тази функция върни вторият му символ -> arrayElement[1].". Т.е. в ляво имаме аргументите, в дясно return-а. В РНР за щастие или не, записът е по-пълен

$newArr = array_map(
    function($arrayElement) {
        return $arrayElement[1];
    },
    $arr
);

Анонимно или не, map/Select, не е вграденият метод, който ни върши работа. Този метод просто трансформира масив с едни елементти, в масив със същия на брой, но евентуално друг тип и стойност елементи. Ако искаме да свием масива, както успяхме да го направим ръчно, трябва да използваме команда за свиване по критерий, позната още като филтрация. В C# това го наричат Where(). В Java и РНР се нарича filter().

Нека тогава от масива с имената, изкараме само тези, които съдържат "o". Обикновено филтриращата функция очаква функция, която връща true или false. Ако върне true, елементите ще се запишат в резултатния масив. Ако върне false - няма.

            string[] arr = {"pesho", "    ", "gosho", "  ", "   ", "petko", "mariq", "    "};
            string[] newArr = arr.Where(arrayElement =>
            {
                if (arrayElement.Contains("o"))
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }).ToArray();

Какво казваме тук? За всеки един "arrayElement" провери дали съдържа О. Ако съдържа - върни вярно, в противен случай невярно. И всъщност само за тези, за които върне вярно ще се запишат в newArr. А именто "pesho", "gosho" и "petko".

Разбира се, Contains() връща директно true/false, така че можем да го сведем до подобно на горният запис, който направихме за Select, а именно:

            string[] newArr = arr.Where(arrayElement => arrayElement.Contains("o")).ToArray();

В РНР за Contains() често се ползва "strstr()" функцията. Както казахме, там Where се нарича filter. Той подобно на C# очаква функция, било то именована или анонимна, която да върне true/false.

$newArr = array_filter(
    $arr, 
    function($arrayElement) {
        return strstr($arrayElement, "o");
    }
);

Резултатът отново е същият:

array(3) {
  [0]=>
  string(5) "pesho"
  [2]=>
  string(5) "gosho"
  [5]=>
  string(5) "petko"
}

Та, какво заключение можем да си направим? Щом успяхме с filter() да махнем тези, които не съдържат "o", то вероятно можем да махнем и тези, които са празни. Нека пробваме с empty(trim()), както в първоначалният вариант:

$newArr = array_filter(
    $arr,
    function($arrayElement) {
        return !empty(trim($arrayElement));
    }
);

Ииии, воала:

array(4) {
  [0]=>
  string(5) "pesho"
  [2]=>
  string(5) "gosho"
  [5]=>
  string(5) "petko"
  [6]=>
  string(5) "mariq"
}

P.S.: Предвид факта, че си карал Programming Basics/Fundamentals е нещо като MUST да се сетиш поне първият вариант, в който ръчно ги обхождаш с foreach. Виж дали нямаш някакви пропуски в материала по основи на програмирането, тъй като ще ти е нужен за всяко по-сложно нещо.

Успех и попътен вятър!

5
04/08/2016 01:54:24
VladoGenov avatar VladoGenov 44 Точки

Благодаря много за изчерпателния отговор!
Беше доста полезен!
Иначе бих могъл да реализирам варианта с последователни операции, но мислех, че има кратък вариант с готова функция, като същевременно мислех, че trim() премахва и празните редове в получения стринг/масив, но всъщност ми се изясни, че тя действа сами върху определения стринг/елемент от масива. И в реда на тези си мисли, реших, че вероятно не ползвам функцията array_map() коректно и че може като първи параметър да се постави условие от рода на

("/[\s,]+/",$_GET['key-value-pairs']);

което да премахва и празните редове.
Сега ми е ясно вече действието на array_map(), и trim(), за което благодаря!
Ако бих могъл да попитам и още нещо:
Защо проверката 

if (isset($_GET['delimiter']) && isset($_GET['array-size']) && isset($_GET['key-value-pairs'])){
// We have data in Input fields ...!!!
 }

винаги дава TRUE след натискане на submit, дори и ПРИ ПРАЗНИ, НЕПОПЪЛНЕНИ данни в полетата?!
Примерно независимо дали съм въвел или не някаква стойност в полето, натискайки submit, isset($_GET['delimiter']) да речем, винаги ми дава TRUE, което малко обезсмисля тази проверка?!
Дали нещо друго влияе в случая или isset() има друга идея?

0
RoYaL avatar RoYaL SoftUni Team Trainer 6796 Точки

isset() проверява дали дадена променлива/стойност присъства в текущия scope. Променливи, които са празни стрингове, все пак съществуват.

Направи си следния експеримент.

<form>
    <input type="text" name="lastName"/>
    <input type="text" name="delimiter"/>
    <input type="submit"/>
</form>

въведи нещо само в едното поле или не въвеждай нищо и кликни бутона Submit Query. Погледни резултата в URL-a (Address bar-a):

http://royal.local/test.php?lastName=&delimiter=

Както се забелязва и lastName и delimiter са там, но са с празни стойности.

Всъщност използването на isset() е крайно обезкуражени от добрите практики. Би трябвало на един endpoint (страница, метод...) да си абсолютно убеден за данните, които ще получиш и да няма нужда да проверяваш там ли са или не са. Т.е. само на едно централно място да се прави проверка за request method-а и после данните да се разпращат на определени методи, които ги очакват със сигурност. Но това е друга бира и няма да я засягам повече в темата.

Идеята в случая с $_GET/$_POST е, че евентуално може да има два или повече формуляра, които изпращат заявка до страницата ти.

<form>
    <input type="text" name="lastName"/>
    <input type="text" name="delimiter"/>
    <input type="submit"/>
</form>

<form>
    <input type="text" name="firstName"/>
    <input type="text" name="delimiter"/>
    <input type="submit"/>
</form>

<form>
    <input type="text" name="username"/>
    <input type="text" name="password"/>
    <input type="submit"/>
</form>

Ако кликна бутона на първия, ще се изпратят lastName и delimiter. Ако кликна на втория - firstName и delimiter. Респективно на третия - username и password.

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

if (isset($_GET['lastName'], $_GET['delimiter'])) {
    echo "първият формуляр е изпратен";
} else if (isset($_GET['firstName'], $_GET['delimiter'])) {
    echo "вторият формуляр е изпратен";
} else if (isset($_GET['username'], $_GET['password'])) {
    echo "третият формуляр е изпратен";
} else {
    echo "не е изпратен формуляр";
}

Естествено, ако имаш повече от две полета, става крайно неподдържано това да изреждаш всичките в isset() (един от проблемите на тази практика). Тогава по-логично би било да сложиш уникален елемент във всеки формуляр и да проверяваш него. Например бутонът за изпращане на формуляра.

<form>
    <input type="text" name="lastName"/>
    <input type="text" name="delimiter"/>
    <input type="submit" name="form1"/>
</form>

<form>
    <input type="text" name="firstName"/>
    <input type="text" name="delimiter"/>
    <input type="submit" name="form2"/>
</form>

<form>
    <input type="text" name="username"/>
    <input type="text" name="password"/>
    <input type="submit" name="form3"/>
</form>
if (isset($_GET['form1'])) {
    echo "първият формуляр е изпратен";
} else if (isset($_GET['form2'])) {
    echo "вторият формуляр е изпратен";
} else if (isset($_GET['form3'])) {
    echo "третият формуляр е изпратен";
} else {
    echo "не е изпратен формуляр";
}

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

if (!empty($_GET['firstName'])) {
    echo "първо име от втори формуляр : " . $_GET['firstName'];
}

 

Що се отнася до хитрия начин за филтриране на празните елементи в масив, можем да опрем до това как работят нетипизираните езици като РНР. При тях има type-juggling. Когато се опитваш да провериш нещо от един тип дали има стойност от друг тип, то това нещо тихичко се каства до втория тип. Така например условието на if() блок трябва да бъде булево, но ако бъде подадена друга стойност, тя ще се кастне до булева.

$value1 = true;
$value2 = "something";
$value3 = "";
$value4 = 0;

if ($value1) {
    echo "ок 1<br/>";
}

if ($value2) {
    echo "ок 2<br/>";
}

if ($value3) {
    echo "ок 3<br/>";
}


if ($value4) {
    echo "ок 4<br/>";
}

Празен стринг и нула са FALSE by nature, ако бъдат кастнати към булева стойност. В такъв случай, ако имаме в един масив празни стрингове, нули или други falsy стойности (разбирай под falsy всичко, което като го кастнеш към булева стойност връща FALSE).

Така, че нека си представим, че имаме масива:

$arr = ["", 0, "asd", false, "bla", null];

И искаме да махнем всички falsy стойности, а именно: "", 0, false, null;

Ще използваме array_filter() като функцията, която ще подадем ще провери дали кастнатия към boolean елемент от масива връща true и ще върнем true, в противен случай false. Само когато функцията върне true тогава елементът ще се запази.

$arr = array_filter(
    $arr,
    function($arrayElement) {
        if ($arrayElement) {
            return true;
        } else {
            return false;
        }
    }
);

Резултат:

array(2) {
  [2]=>
  string(3) "asd"
  [4]=>
  string(3) "bla"
}

Нека опростим функцията. if (el) return true може да се свете до просто return el, тъй като в условието на иф-а се крие и return-a:

$arr = array_filter(
    $arr,
    function($arrayElement) {
       return $arrayElement;
    }
);

А сега нека прочетем документацията на функцията array_filter. Вариант е ако ползваш сносно IDE да я кликнеш (CTRL+click при PHPStorm). Другият вариант е да я отвориш в php.net. Нека видим какво пише над функцията когато е CTRL+click-на

/**
 * Iterates over each value in the <b>array</b>
 * passing them to the <b>callback</b> function.
 * If the <b>callback</b> function returns true, the
 * current value from <b>array</b> is returned into
 * the result array. Array keys are preserved.
 * @link http://php.net/manual/en/function.array-filter.php
 * @param array $input <p>
 * The array to iterate over
 * </p>
 * @param callback $callback [optional] <p>
 * The callback function to use
 * </p>
 * <p>
 * If no callback is supplied, all entries of
 * input equal to false (see
 * converting to
 * boolean) will be removed.
 * </p>

Т.е. можем да пропуснем втория аргумент на функцията, който е нашата функция какво да остане в масива. Ако го направим, всички falsy елементи така или иначе ще изчезнат.

$arr = array_filter($arr);

Резултатът отново е същият!

В крайна сметка ако се озовем с масива от първия случай:

$arr = ["pesho", "    ",  "gosho", "", "  ", "petko", "mariq", ""];

Трябва да сведем всички whitespaces до празен стринг, за да мине горният номер. Тук идва твоят array_map. Ще тримнем всеки елемент и после ще го филтрираме.

$arr = ["pesho", "    ",  "gosho", "", "  ", "petko", "mariq", ""];
$arr = array_map('trim', $arr);
$arr = array_filter($arr);

Резултат:

array(4) {
  [0]=>
  string(5) "pesho"
  [2]=>
  string(5) "gosho"
  [5]=>
  string(5) "petko"
  [6]=>
  string(5) "mariq"
}

Иии разбира се, ако сме мазачи, можем и на един ред да го напишем :-))

$arr = array_filter(array_map('trim', $arr));

И резултатът отново е този, който желаем ;-)

2
VladoGenov avatar VladoGenov 44 Точки

Благодаря отново за изчерпателния отговор!
Беше ми много полезен.
Съжалявам само че под отговора нямаше този път бутон <Best answer> smiley

0
RoYaL avatar RoYaL SoftUni Team Trainer 6796 Точки

Map-а ще мине през всеки стрингов елемент в масива и ще изпълни функцията trim(). Което ще махне празните места от ляво и от дясно на всеки елемент. Елементите съставени само от празни места ще бъдат сведени до празни стрингове. Все пак ще съществуват като елементи в масива. Дали не трябва да предприемеш друго действие за да се отървеш от тези елементи, които са празни?

0
VladoGenov avatar VladoGenov 44 Точки

Да, точно това ми е нужно.
Някаква идея или насока, има ли някаква готова функция или по-скоро последователни операции да ползвам за премахване на празния елемент?

0