Задача: Анализ логов заказов
Glossary overview

Задача: Анализ логов заказов

Представь, что у тебя есть логи заказов интернет-магазина в виде строк.

📥 Входные данные

У тебя есть массив строк:

$orders = [
    "order_id=1001;user=alex;total=250.50;status=paid",
    "order_id=1002;user=john;total=99.99;status=pending",
    "order_id=1003;user=alex;total=15.00;status=paid",
    "order_id=1004;user=maria;total=300;status=failed",
    "order_id=1005;user=john;total=120;status=paid",
];

Твоя задача

1️⃣ Распарсить данные

Преобразовать каждую строку в ассоциативный массив:

[
  'order_id' => 1001,
  'user' => 'alex',
  'total' => 250.50,
  'status' => 'paid'
]

2️⃣ Отфильтровать оплаченные заказы

Оставить только заказы со статусом paid.

3️⃣ Посчитать статистику по пользователям

Сформировать массив вида:

[
  'alex' => [
      'orders' => 2,
      'total_sum' => 265.50
  ],
  'john' => [
      'orders' => 1,
      'total_sum' => 120.00
  ]
]

4️⃣ Найти пользователя с максимальным числом заказов

Вывести:

alex (2)

Первое решение (черновое)

<?php

$orders = [
    "order_id=1001;user=alex;total=250.50;status=paid",
    "order_id=1002;user=john;total=99.99;status=pending",
    "order_id=1003;user=alex;total=15.00;status=paid",
    "order_id=1004;user=maria;total=300;status=failed",
    "order_id=1005;user=john;total=120;status=paid",
];


$result = array_map(function($order) {
	$orderDetails = explode(";", $order);
	$keys = [];
	$values = [];
	
	foreach ($orderDetails as $item) {
		$item = explode("=", $item);

		$keys[] = $item[0];
		$values[] = $item[1];
		
	}
	
	$orderAssoc = array_combine($keys, $values);

	return $orderAssoc;
}, $orders);

// Оставить только заказы со статусом paid.
	
$orderDetailsFiltered = array_filter($result, function($arr) {
	
	return $arr['status'] == 'paid';
});

// Посчитать статистику по пользователям

$ordersByUsers = [];

foreach($orderDetailsFiltered as $order) {
	$user = $order['user'];
	$alreadyInArray = in_2d_array($user, $orderDetailsFiltered);

	if ($alreadyInArray) {
		$ordersByUsers[$user] = [
			'orders' =>	$ordersByUsers[$user]['orders'] + 1,
			'total_sum' => $ordersByUsers[$user]['total_sum'] + $order['total']
		];
	} else {
		$ordersByUsers[$user] = [
			'orders' => 1,
			'total_sum' => $order['total']
		];
	}
}

// Найти пользователя с максимальной суммой заказов
uasort($ordersByUsers, function($a, $b) {
      return $b['orders'] <=> $a['orders'];
});

$topUser = array_key_first($ordersByUsers);
$topOrders = $ordersByUsers[$topUser]['orders'];

$result = $topUser . '(' . $topOrders . ')';



function in_2d_array($needle, $haystack) {
    foreach($haystack as $element) {
   
        if(in_array($needle, $element))
            return true;
    }
    return false;
}


print_r($result);

// Output
alex(2)

Важные замечания:

  • Используем uasort, а не usort чтобы сохранить ключи массива. Вот так выглядит массив после сортировки с помощью usort. Он ПЕРЕИНДЕКСИРУЕТ ключи в 0, 1, 2 и возвращает true / false.
Array
(
    [0] => Array
        (
            [orders] => 2
            [total_sum] => 265.5
        )

    [1] => Array
        (
            [orders] => 1
            [total_sum] => 120
        )

)
  • uasort сохраняет ключи.
Array
(
    [alex] => Array
        (
            [orders] => 2
            [total_sum] => 265.5
        )

    [john] => Array
        (
            [orders] => 1
            [total_sum] => 120
        )

)
  • Методы сортировки возвращают true / false и меняют массив, поэтому не надо присваивать результат в переменную.
$result = uasort($ordersByUsers, function($a, $b) {
      return $b['orders'] <=> $a['orders'];
});

// Output
1
  • array_combine() формирует массив с ключей и значений
$orderAssoc = array_combine($keys, $values);

Рефакторинг кода.

  • Можно получать значения с помощью деструктурирующего присваивания массива – Array destructuring (деструктуризация массива).
$orderDetails = explode(";", $order);
$keys = [];
$values = [];
	
foreach ($orderDetails as $item) {
	// $item = explode("=", $item); 
	
        /*Получаем массив ["order_id", "1001"]*/
	/*Можем сразу положить в переменные*/
	[$key, $value] = explode("=", $item, 2);

	// $keys[] = $item[0];
	// $values[] = $item[1];
		
	$keys[] = $key;
	$values[] = $value;
		
}

Говорим PHP: «Возьми 0-й элемент массива → положи в $key. Возьми 1-й элемент массива → положи в $value».

  • Я каждый раз пересобираю массив пользователя.
$ordersByUsers[$user] = [
        'orders' =>	$ordersByUsers[$user]['orders'] + 1,
	'total_sum' => $ordersByUsers[$user]['total_sum'] + $order['total']
		];

Можно обновлять существующие значения, это чище:

$ordersByUsers[$user]['orders']++;
$ordersByUsers[$user]['total_sum'] += (float)$order['total'];
  • Нет смысла в функции in_2d_array.
$ordersByUsers = [];

foreach($orderDetailsFiltered as $order) {
	$user = $order['user'];
	$alreadyInArray = in_2d_array($user, $orderDetailsFiltered);

	if ($alreadyInArray) {
		$ordersByUsers[$user]['orders']++;
		$ordersByUsers[$user]['total_sum'] += (float)$order['total'];
	} else {
		$ordersByUsers[$user] = [
			'orders' => 1,
			'total_sum' => $order['total']
		];
	}
}

Проверять нужно $ordersByUsers, а не $orderDetailsFiltered.

И вообще in_2d_array() здесь не нужен.

ПРАВИЛЬНЫЙ и ПРОСТОЙ алгоритм

  1. Берём заказ
  2. Смотрим пользователя
  3. Если пользователя ещё нет в $ordersByUsers — создаём
  4. Увеличиваем счётчик и сумму

То есть ситуация такая:

// Пустой массив куда будем записывать
$ordersByUsers = [];


// Массив, по которому проходимся циклом.
Array
(
    [0] => Array
        (
            [order_id] => 1001
            [user] => alex
            [total] => 250.50
            [status] => paid
        )

    [2] => Array
        (
            [order_id] => 1003
            [user] => alex
            [total] => 15.00
            [status] => paid
        )

    [4] => Array
        (
            [order_id] => 1005
            [user] => john
            [total] => 120
            [status] => paid
        )

)

// Из пустого массива надо сделать такой
[
  'alex' => [
      'orders' => 2,
      'total_sum' => 265.50
  ],
  'john' => [
      'orders' => 1,
      'total_sum' => 120.00
  ]
]

Просто будем проверять есть ли в пустом массиве уже элемент с таким именем используя isset.

$ordersByUsers = [];

foreach($orderDetailsFiltered as $order) {
	$user = $order['user'];
	// $alreadyInArray = in_2d_array($user, $orderDetailsFiltered);
	$alreadyInArray = isset($ordersByUsers[$user]);
	
	if ($alreadyInArray) {
		$ordersByUsers[$user]['orders']++;
		$ordersByUsers[$user]['total_sum'] += (float)$order['total'];
	} else {
		$ordersByUsers[$user] = [
			'orders' => 1,
			'total_sum' => $order['total']
		];
	}
}

Теперь решение задачи выглядит так.

<?php

$orders = [
    "order_id=1001;user=alex;total=250.50;status=paid",
    "order_id=1002;user=john;total=99.99;status=pending",
    "order_id=1003;user=alex;total=15.00;status=paid",
    "order_id=1004;user=maria;total=300;status=failed",
    "order_id=1005;user=john;total=120;status=paid",
];


$result = array_map(function($order) {
	$orderDetails = explode(";", $order);
	$keys = [];
	$values = [];
	
	foreach ($orderDetails as $item) {
		[$key, $value] = explode("=", $item, 2);
		
		$keys[] = $key;
		$values[] = $value;
		
	}
	
	$orderAssoc = array_combine($keys, $values);

	return $orderAssoc;
}, $orders);

// Оставить только заказы со статусом paid.
	
$orderDetailsFiltered = array_filter($result, function($arr) {
	
	return $arr['status'] == 'paid';
});

// Посчитать статистику по пользователям

$ordersByUsers = [];

foreach($orderDetailsFiltered as $order) {
	$user = $order['user'];
	// $alreadyInArray = in_2d_array($user, $orderDetailsFiltered);
	$alreadyInArray = isset($ordersByUsers[$user]);
	
	if ($alreadyInArray) {
		$ordersByUsers[$user]['orders']++;
		$ordersByUsers[$user]['total_sum'] += (float)$order['total'];
	} else {
		$ordersByUsers[$user] = [
			'orders' => 1,
			'total_sum' => (float)$order['total']
		];
	}
}

// Найти пользователя с максимальной суммой заказов
uasort($ordersByUsers, function($a, $b) {
      return $b['orders'] <=> $a['orders'];
});

$topUser = array_key_first($ordersByUsers);
$topOrders = $ordersByUsers[$topUser]['orders'];

$result = $topUser . '(' . $topOrders . ')';


print_r($result);

Найти ТОП-пользователя БЕЗ сортировки (O(n))

Плохой вариант (то, что было):

uasort($ordersByUsers, ...); // O(n log n)
$topUser = array_key_first($ordersByUsers);

Работает, но лишняя работа.

Правильный вариант: один проход

Идея:
  • идём по массиву
  • храним текущий максимум
  • сравниваем
$topUser = null;
$maxOrders = 0;

foreach ($ordersByUsers as $user => $stats) {
    if ($stats['orders'] > $maxOrders) {
        $maxOrders = $stats['orders'];
        $topUser = $user;
    }
}

echo $topUser . '(' . $maxOrders . ')';

Финальное решение

<?php

$orders = [
    "order_id=1001;user=alex;total=250.50;status=paid",
    "order_id=1002;user=john;total=99.99;status=pending",
    "order_id=1003;user=alex;total=15.00;status=paid",
    "order_id=1004;user=maria;total=300;status=failed",
    "order_id=1005;user=john;total=120;status=paid",
];


$result = array_map(function($order) {
	$orderDetails = explode(";", $order);
	$keys = [];
	$values = [];
	
	foreach ($orderDetails as $item) {
		[$key, $value] = explode("=", $item, 2);
		
		$keys[] = $key;
		$values[] = $value;
		
	}
	
	$orderAssoc = array_combine($keys, $values);

	return $orderAssoc;
}, $orders);

// Оставить только заказы со статусом paid.
	
$orderDetailsFiltered = array_filter($result, function($arr) {
	
	return $arr['status'] == 'paid';
});

// Посчитать статистику по пользователям

$ordersByUsers = [];

foreach($orderDetailsFiltered as $order) {
	$user = $order['user'];
	// $alreadyInArray = in_2d_array($user, $orderDetailsFiltered);
	$alreadyInArray = isset($ordersByUsers[$user]);
	
	if ($alreadyInArray) {
		$ordersByUsers[$user]['orders']++;
		$ordersByUsers[$user]['total_sum'] += (float)$order['total'];
	} else {
		$ordersByUsers[$user] = [
			'orders' => 1,
			'total_sum' => (float)$order['total']
		];
	}
}

// Найти пользователя с максимальной суммой заказов
$topUser = null;
$maxOrders = 0;

foreach ($ordersByUsers as $user => $stats) {
    if ($stats['orders'] > $maxOrders) {
        $maxOrders = $stats['orders'];
        $topUser = $user;
    }
}

$result = $topUser . '(' . $maxOrders . ')';


print_r($result);