Faster way to process xml file using PHP - php

I have this xml file named flight-itinerary.xml. A scaled-down version is shown below.
<itin line="1" dep="LOS" arr="ABV">
<flt>
<fltav>
<cb>1</cb>
<id>C</id>
<av>10</av>
<cur>NGN</cur>
<CurInf>2,0.01,0.01</CurInf>
<pri>15000.00</pri>
<tax>30800.00</tax>
<fav>1</fav>
<miles></miles>
<fid>11</fid>
<finf>0,0,1</finf>
<cb>2</cb>
<id>J</id>
<av>10</av>
<cur>NGN</cur>
<CurInf>2,0.01,0.01</CurInf>
<pri>13000.00</pri>
<tax>26110.00</tax>
<fav>1</fav>
<miles></miles>
<fid>12</fid>
<finf>0,0,0</finf>
</fltav>
</flt>
</itin>
The complete file contains 8 itinerary <itin> elements. The <fltav> element of each of the <itin> elements contains 11 of the <cb>1</cb> to <finf>0,0,1</finf> groups.
And below is the code I am using to process the file:
<?php
function processFlightsData()
{
$data = array();
$dom= new DOMDocument();
$dom->load('flight-itinerary.xml');
$classbands = $dom->getElementsByTagName('classbands')->item(0);
$bands = $classbands->getElementsByTagName('band');
$itineraries = $dom->getElementsByTagName('itin');
$counter = 0;
foreach($itineraries AS $itinerary)
{
$flt = $itinerary->getElementsByTagName('flt')->item(0);
$dep = $flt->getElementsByTagName('dep')->item(0)->nodeValue;
$arr = $flt->getElementsByTagName('arr')->item(0)->nodeValue;
$time_data = $flt->getElementsByTagName('time')->item(0);
$departure_day = $time_data->getElementsByTagName('ddaylcl')->item(0)->nodeValue;
$departure_time = $time_data->getElementsByTagName('dtimlcl')->item(0)->nodeValue;
$departure_date = $departure_day. ' '. $departure_time;
$arrival_day = $time_data->getElementsByTagName('adaylcl')->item(0)->nodeValue;
$arrival_time = $time_data->getElementsByTagName('atimlcl')->item(0)->nodeValue;
$arrival_date = $arrival_day. ' '. $arrival_time;
$flight_duration = $time_data->getElementsByTagName('duration')->item(0)->nodeValue;
$flt_det = $flt->getElementsByTagName('fltdet')->item(0);
$airline_id = $flt_det->getElementsByTagName('airid')->item(0)->nodeValue;
$flt_no = $flt_det->getElementsByTagName('fltno')->item(0)->nodeValue;
$flight_number = $airline_id. $flt_no;
$airline_type = $flt_det->getElementsByTagName('eqp')->item(0)->nodeValue;
$stops = $flt_det->getElementsByTagName('stp')->item(0)->nodeValue;
$av_data = $flt->getElementsByTagName('fltav')->item(0);
$cbs = iterator_to_array($av_data->getElementsByTagName('cb')); //11 entries
$ids = iterator_to_array($av_data->getElementsByTagName('id')); //ditto
$seats = iterator_to_array($av_data->getElementsByTagName('av')); //ditto
$curr = iterator_to_array($av_data->getElementsByTagName('cur')); //ditto
$price = iterator_to_array($av_data->getElementsByTagName('pri')); //ditto
$tax = iterator_to_array($av_data->getElementsByTagName('tax')); //ditto
$miles = iterator_to_array($av_data->getElementsByTagName('miles')); //ditto
$fid = iterator_to_array($av_data->getElementsByTagName('fid')); //ditto
$inner_counter = 0;
for($i = 0; $i < count($ids); $i++)
{
$data[$counter][$inner_counter] = array
(
'flight_number' => $flight_number,
'flight_duration' => $flight_duration,
'departure_date' => $departure_date,
'departure_time' => substr($departure_time, 0, 5),
'arrival_date' => $arrival_date,
'arrival_time' => substr($arrival_time, 0, 5),
'departure_airport_code' => $dep,
'departure_airport_location_name' => get_airport_data($dep, $data_key='location'),
'arrival_airport_code' => $arr,
'arrival_airport_location_name' => get_airport_data($arr, $data_key='location'),
'stops' => $stops,
'cabin_class' => $ids[$i]->nodeValue,
'ticket_class' => $ids[$i]->nodeValue,
'ticket_class_nicename' => formate_ticket_class_name($ids[$i]->nodeValue),
'available_seats' => $seats[$i]->nodeValue,
'currency' => $curr[$i]->nodeValue,
'price' => $price[$i]->nodeValue,
'tax' => $tax[$i]->nodeValue,
'miles' => $miles[$i]->nodeValue,
);
++$inner_counter;
}
return $data;
}
?>
Now, the outer loop iterates 8 times for each <itin> element, and during each iteration of the outer loop, the inner loop iterates 11 times, resulting in a total of 88 iterations per pass and causing serious performance issues. What I am looking for is a faster method of processing the file. Any helps will be greatly appreciated.

I don't think the loop is the bottle-neck. You should check your operations that are called within the loop, get_airport_data and formate_ticket_class_name.
Trying your code (without the auxiliary operations) on a number of itin elements takes less than a second, check this fiddle: http://phpfiddle.org/main/code/7fpi-b3ka (Note that the XML might not be similar to yours, I've guessed a lot of elements that were missing).
If there are operations that are called which increases the processing time substantially, try to call the operation with bulk data or cache the responses.

Related

Writing json to file, only one line written

I have this code i am using to write json to a file
<?php
require_once 'vendor/autoload.php';
$faker = Faker\Factory::create();
function getAge($start = 18, $end = 100, $repeat = 20){
$result = null;
static $ages = array();
if ( empty($ages) ) {
for ($i= $start; $i <= $end; $i++) {
for($j = 0; $j < $repeat; $j++)
$ages[] = $i;
}
}
$index = rand(0, count($ages));
$result = $ages[ $index ];
unset($ages[ $index ]);
$ages = array_values($ages);
return $result;
}
$superstars = array("Adam Cole","Finn Balor","Pete Dunne","Jordan Devlin","Noam Dar");
$fan_favourite_superstar_name = $superstars[ mt_rand( 0, count($superstars) -1 ) ];
$cities = array("London","Manchester","Leeds","Bristol");
$fan_location = $cities[ mt_rand( 0, count($cities) -1 ) ];
$the_models = array("Iphone","Nokia","Huawei","Samsung");
$fan_phone_model = $the_models[ mt_rand( 0, count($the_models) -1 ) ];
for ($x = 1; $x <= 100; $x++) {
echo $x;
$array = Array (
"$x" => Array (
"id" => uniqid(),
"fan_favourite_superstar_name" => $fan_favourite_superstar_name,
"fan_location" => $fan_location,
"fan_phone_model" => $fan_phone_model,
"fan_name" => $faker->name,
"fan_age" => getAge(),
"fan_comments" => $faker->text,
"fan_picture" => rand(1,500),
"last_updated" => time() + rand(1,1000),
)
);
// encode array to json
$json = json_encode(array('fans' => $array));
//write json to file
if (file_put_contents("data.json", $json))
echo "JSON file created successfully..."."<br/>";
else
echo "Oops! Error creating json file...";
}
?>
When i run, only the first line is written. Why are the other lines not being written?.
You are overwriting the file in each iteration of the loop. You need to tell the file_put_contents function you want to append, like so:
file_put_contents("data.json", $json, FILE_APPEND)
Edit:
Even though this answers the posted question, As Nigel Ren pointed out, this won't create a valid JSON file. Concerning your follow-up comment about the desired format, you'd need to build your array inside the loop and then write once afterwards (by moving everything after $json = json_encode(array('fans' => $array)); outside of the loop).
The correct way to build it is to follow Nigel's first comment about assignment (something the other answers which were deleted in the meantime also dealt with) to avoid overwriting the array each time:
$array["$x"] = Array (
"id" => uniqid(),
...
So, ultimately, you wouldn't even need the appending flag.

CodeIgniter one query multiple statements

I use CodeIgniter, and when an insert_batch does not fully work (number of items inserted different from the number of items given), I have to do the inserts again, using insert ignore to maximize the number that goes through the process without having errors for existing ones.
When I use this method, the kind of data I'm inserting does not need strict compliance between the number of items given, and the number put in the database. Maximize is the way.
What would be the correct way of a) using insert_batch as much as possible b) when it fails, using a workaround, while minimizing the number of unnecessary requests?
Thanks
The Correct way of inserting data using insert_batch is :
CI_Controller :
public function add_monthly_record()
{
$date = $this->input->post('date');
$due_date = $this->input->post('due_date');
$billing_date = $this->input->post('billing_date');
$total_area = $this->input->post('total_area');
$comp_id = $this->input->post('comp_id');
$unit_id = $this->input->post('unit_id');
$percent = $this->input->post('percent');
$unit_consumed = $this->input->post('unit_consumed');
$per_unit = $this->input->post('per_unit');
$actual_amount = $this->input->post('actual_amount');
$subsidies_from_itb = $this->input->post('subsidies_from_itb');
$subsidies = $this->input->post('subsidies');
$data = array();
foreach ($unit_id as $id => $name) {
$data[] = array(
'date' => $date,
'comp_id' => $comp_id,
'due_date' => $due_date,
'billing_date' => $billing_date,
'total_area' => $total_area,
'unit_id' => $unit_id[$id],
'percent' =>$percent[$id],
'unit_consumed' => $unit_consumed[$id],
'per_unit' => $per_unit[$id],
'actual_amount' => $actual_amount[$id],
'subsidies_from_itb' => $subsidies_from_itb[$id],
'subsidies' => $subsidies[$id],
);
};
$result = $this->Companies_records->add_monthly_record($data);
//return from model
$total_affected_rows = $result[1];
$first_insert_id = $result[0];
//using last id
if ($total_affected_rows) {
$count = $total_affected_rows - 1;
for ($x = 0; $x <= $count; $x++) {
$id = $first_insert_id + $x;
$invoice = 'EBR' . date('m') . '/' . date('y') . '/' . str_pad($id, 6, '0', STR_PAD_LEFT);
$field = array(
'invoice_no' => $invoice,
);
$this->Companies_records->add_monthly_record_update($field,$id);
}
}
echo json_encode($result);
}
CI_Model :
public function add_monthly_record($data)
{
$this->db->insert_batch('monthly_record', $data);
$first_insert_id = $this->db->insert_id();
$total_affected_rows = $this->db->affected_rows();
return [$first_insert_id, $total_affected_rows];
}
AS #q81 mentioned, you would split the batches (as you see fit or depending on system resources) like this:
$insert_batch = array();
$maximum_items = 100;
$i = 1;
while ($condition == true) {
// code to add data into $insert_batch
// ...
// insert the batch every n items
if ($i == $maximum_items) {
$this->db->insert_batch('table', $insert_batch); // insert the batch
$insert_batch = array(); // empty batch array
$i = 0;
}
$i++;
}
// the last $insert_batch
if ($insert_batch) {
$this->db->insert_batch('table', $insert_batch);
}
Edit:
while insert batch already splits the batches, the reason why you have "number of items inserted different from the number of items given" might be because the allowed memory size is reached. this happened to me too many times.

calculate avg return "0" PHP

I have MySQL with Float Values which are pressure parametters. I need to calculate average. But the code I make returns me average "empty" in my json response.
Here is the json response I get:
{"result":{"pressure":{"min":[{"Id":"2","presion":"0","Insertado":"2016-08-16 16:20:08"},{"Id":"5","presion":"0","Insertado":"2016-08-16 18:09:04"}],"max":[{"Id":"3","presion":"55","Insertado":"2016-08-16 16:22:14"}],"avg":[]},"last_entry":{"Id":"8","presion":"50","Insertado":"2016-08-16 18:28:45"}}}
As it shows. avg give empty!.
This is code in PHP
$press_values = get_press_values($json_object->result);
// get pressure result set with respected values
$press_result = get_press_result_set_from_values($json_object->result,$press_values);
// get latest entry
$latest_entry = get_latest_date_entry($json_object->result);
// Wrap results in an array
$output_result = array(
'pressure' => $press_result,
'last_entry' => $latest_entry
);
}
Then.
function get_press_values($result){
$min = -1;
$max = -1;
$avg = -1;
// get all pressure values
$pressures = array_map(function($result_item) {
return intval($result_item->presion);
}, $result);
if($pressures){
$min = min($pressures);
$max = max($pressures);
$avg = intval(calculate_average($pressures));
}
return array(
'min' => $min,
'max' => $max,
'avg' => $avg
);
}
Then:
function get_press_result_set_from_values($array,$value){
$min_objs = array();
$max_objs = array();
$avg_objs = array();
foreach ($array as $item) {
if($item->presion == $value['min']){
$min_objs[] = $item;
}
if($item->presion == $value['max']){
$max_objs[] = $item;
}
if($item->presion == $value['avg']){
$avg_objs[] = $item;
}
}
return array(
'min' => $min_objs,
'max' => $max_objs,
'avg' => $avg_objs,
);
}
then:
function calculate_average($arr) {
$total = 0;
$count = count($arr); //total numbers in array
foreach ($arr as $value) {
$total = $total + $value; // total value of array numbers
}
$average = ($total/$count); // get average value
return $average;
}
This part in your code:
if($item->presion == $value['avg']){
$avg_objs[] = $item;
}
will only add an item if the presion perfectly matches the average, which can be very unlikely, depending on your data of course.
update: For example you have the values 50, 51, 54, 55. The average is 53. Your code will not find any $item->presion which is equal to 53, because the average doesn't need an item that has that value (in contrast to min and max).
You should check if you actually mean the median (which has to be determined in a different way ...).
update2: to get the average into your result, you have to change
$avg_objs = array(); to $avg_objs = $value['avg'];. and remove the part from your code that I posted above.
Also, if you want your average to not be an integer, you should change the line
$average = ($total/$count);
to
$average = (float)$total / $count;
and remove the intval from:
$avg = intval(calculate_average($pressures));

Loop through undetermined amout of chained arrays

I know basic PHP and all I know I've learned on google during the past years.
With that said, I'm developing a new website and I'm experimenting with clean URLs for SEO purposes and multi-regional languages compatibility.
So I have this array:
$list['requests']['en'] = array(
'index' => array(
0,
'subscribe',
'search'
),
'services' => array(
0,
'next' => array(
'service1',
'service2',
'service3'
),
'subscribe',
'search'
),
'service1' => array(
0,
'subscribe'
),
'service2' => array(
1,
'next' => array(
'view'
),
'jump' => 1,
'subscribe',
'search'
),
'service3' => array(
1,
'subscribe',
),
'view' => array(
2,
'subscribe'
),
'bad-request' => array(
0,
'subscribe'
)
);
Where $list['requests']['en'][$page][0] is the level of the page in the url and $list['requests']['en'][$page]['next'] is a list of valid pages that can follow.
That array is used to validate a URLs, so this is a valid one: www.website.com/services/service2/view
My real problem is that I want to loop throug that array and follow every page in the 'next' list so I have a function to create files like this: en/services.service1.view.php
I alreday created a chain of loops that do the job, but it's not generic. It's limited to the amount of loops I type in myself. This code for example, only runs throug 4 loops:
if(isset($list['requests'][$lang][$list_keys[$j]]['next'])){
for($l = 0; !empty($list['requests'][$lang][$list_keys[$j]]['next'][$l]) && $l < $for_max; $l++){
$next = $list['requests'][$lang][$list_keys[$j]]['next'][$l];
$file_next = $file[0].'.'.$next;
$file[] = $file_next;
if(isset($list['requests'][$lang][$next]['next'])){
for($n = 0; !empty($list['requests'][$lang][$next]['next'][$n]) && $n < $for_max; $n++){
$next_1 = $list['requests'][$lang][$next]['next'][$n];
$file_next_1 = $file_next.'.'.$next_1;
$file[] = $file_next_1;
if(isset($list['requests'][$lang][$next_1]['next'])){
for($o = 0; !empty($list['requests'][$lang][$next_1]['next'][$o]) && $o < $for_max; $o++){
$next_2 = $list['requests'][$lang][$next_1]['next'][$o];
$file_next_2 = $file_next_1.'.'.$next_2;
$file[] = $file_next_2;
if(isset($list['requests'][$lang][$next_2]['next'])){
for($p = 0; !empty($list['requests'][$lang][$next_2]['next'][$p]) && $p < $for_max; $p++){
$next_3 = $list['requests'][$lang][$next_2]['next'][$p];
$file_next_3 = $file_next_2.'.'.$next_3;
$file[] = $file_next_3;
}
}
}
}
}
}
}
}
This code saves every unique file name from the array chain.
The output would be something like this:
index.php
services.php
services.service1.php
services.service2.php
services.service2.view.php
services.service3.php
...
I've tried to make generic functions to do this but with no success.
Any ideas?
EDIT:
I just came up with an idea to simplify the code with an include. Nevermind my variables, the point here is to make a script that perceives the depth of loops itself.
The hole script for the purpose of context:
<?php
include 'includes/lists/languages.php';
include 'includes/lists/requests.php';
$for_max = 15;
// create requests files for each language
for($i = 0; isset($list['languages'][$i]) && $i < $for_max; $i++){
$lang = $list['languages'][$i];
$requests_keys = array_keys($list['requests'][$lang]);
echo 'LANG: '.$lang.'<br/>';
for($j = 0; isset($list['requests'][$lang][$requests_keys[$j]]) && $j < $for_max; $j++){
if($list['requests'][$lang][$requests_keys[$j]][0] == 0){
$file = array();
$file[] = $requests_keys[$j];
/* NEXT PART LOOP */
$loop = array();
$depth = 0;
$part = array();
$part['next'] = array();
$part['file'] = array();
// these vars are unique for the first loop, then it's generic
$part['next'][$depth-1] = $requests_keys[$j];
$part['file'][$depth-1] = $file[0];
// loop through every 'next' part if it exists in the current part
if(isset($list['requests'][$lang][ $part['next'][$depth-1] ]['next'])){
include '___construct_files.recursive.php';
}
/* CREATE REQUEST FILE */
for($k = 0; $k < count($file); $k++){
$path = 'includes/requests/'.$lang.'/'.$file[$k].'.php';
// string to insert in the file
include '___construct_files.string.php';
// processing
if(!file_exists($path)){
file_put_contents($path, $string);
echo '<br/># created - '.$path.'<br/>';
}else if(file_exists($path)){
echo '<br/># exists - '.$path.'<br/>';
}else{
echo '<br/># error - '.$path.'<br/>';
}
}
}
}
echo '<br/><br/>';
}
?>
The loop is included and verifies if it's needed to be included again in itself (___construct_files.recursive.php):
<?php
for($loop[$depth] = 0; !empty($list['requests'][$lang][ $part['next'][$depth-1] ]['next'][ $loop[$depth] ]) && $loop[$depth] < $for_max; $loop[$depth]++){
$part['next'][$depth] = $list['requests'][$lang][ $part['next'][$depth-1] ]['next'][ $loop[$depth] ];
$part['file'][$depth] = $part['file'][$depth-1].'.'.$part['next'][$depth];
$file[] = $part['file'][$depth];
// if next part is set, increase depth and include loop
if(isset($list['requests'][$lang][ $part['next'][$depth] ]['next'])){
$depth++;
include '___construct_files.recursive.php';
}
}
// decrease depth when higher depth loop ends so the current loop can resume
$depth--;
?>

How do I redistribute an array into another array of a certain "shape". PHP

I have an array of my inventory (ITEMS A & B)
Items A & B are sold as sets of 1 x A & 2 x B.
The items also have various properties which don't affect how they are distributed into sets.
For example:
$inventory=array(
array("A","PINK"),
array("A","MAUVE"),
array("A","ORANGE"),
array("A","GREY"),
array("B","RED"),
array("B","BLUE"),
array("B","YELLOW"),
array("B","GREEN"),
array("B","BLACK")
);
I want to redistribute the array $inventory to create $set(s) such that
$set[0] => Array
(
[0] => array(A,PINK)
[1] => array(B,RED)
[2] => array(B,BLUE)
)
$set[1] => Array
(
[0] => array(A,MAUVE)
[1] => array(B,YELLOW)
[2] => array(B,GREEN)
)
$set[2] => Array
(
[0] => array(A,ORANGE)
[1] => array(B,BLACK)
[2] => NULL
)
$set[3] => Array
(
[0] => array(A,GREY)
[1] => NULL
[2] => NULL
)
As you can see. The items are redistributed in the order in which they appear in the inventory to create a set of 1 x A & 2 x B. The colour doesn't matter when creating the set. But I need to be able to find out what colour went into which set after the $set array is created. Sets are created until all inventory is exhausted. Where an inventory item doesn't exist to go into a set, a NULL value is inserted.
Thanks in advance!
I've assumed that all A's come before all B's:
$inventory=array(
array("A","PINK"),
array("A","MAUVE"),
array("A","ORANGE"),
array("A","GREY"),
array("B","RED"),
array("B","BLUE"),
array("B","YELLOW"),
array("B","GREEN"),
array("B","BLACK")
);
for($b_start_index = 0;$b_start_index<count($inventory);$b_start_index++) {
if($inventory[$b_start_index][0] == 'B') {
break;
}
}
$set = array();
for($i=0,$j=$b_start_index;$i!=$b_start_index;$i++,$j+=2) {
isset($inventory[$j])?$temp1=$inventory[$j]:$temp1 = null;
isset($inventory[$j+1])?$temp2=$inventory[$j+1]:$temp2 = null;
$set[] = array( $inventory[$i], $temp1, $temp2);
}
To make it easier to use your array, you should make it something like this
$inv['A'] = array(
'PINK',
'MAUVE',
'ORANGE',
'GREY'
);
$inv['B'] = array(
'RED',
'BLUE',
'YELLOW',
'GREEN',
'BLACK'
);
This way you can loop through them separately.
$createdSets = $setsRecord = $bTemp = array();
$bMarker = 1;
$aIndex = $bIndex = 0;
foreach($inv['A'] as $singles){
$bTemp[] = $singles;
$setsRecord[$singles][] = $aIndex;
for($i=$bIndex; $i < ($bMarker*2); ++$i) {
//echo $bIndex.' - '.($bMarker*2).'<br/>';
if(empty($inv['B'][$i])) {
$bTemp[] = 'null';
} else {
$bTemp[] = $inv['B'][$i];
$setsRecord[$inv['B'][$i]][] = $aIndex;
}
}
$createdSets[] = $bTemp;
$bTemp = array();
++$bMarker;
++$aIndex;
$bIndex = $bIndex + 2;
}
echo '<pre>';
print_r($createdSets);
print_r($setsRecord);
echo '</pre>';
To turn your array into an associative array, something like this can be done
<?php
$inventory=array(
array("A","PINK"),
array("A","MAUVE"),
array("A","ORANGE"),
array("A","GREY"),
array("B","RED"),
array("B","BLUE"),
array("B","YELLOW"),
array("B","GREEN"),
array("B","BLACK")
);
$inv = array();
foreach($inventory as $item){
$inv[$item[0]][] = $item[1];
}
echo '<pre>';
print_r($inv);
echo '</pre>';
Maybe you can use this function, assuming that:
... $inventory is already sorted (all A come before B)
... $inventory is a numeric array staring at index zero
// $set is the collection to which the generated sets are appended
// $inventory is your inventory, see the assumptions above
// $aCount - the number of A elements in a set
// $bCount - the number of B elements in a set
function makeSets(array &$sets, array $inventory, $aCount, $bCount) {
// extract $aItems from $inventory and shorten $inventory by $aCount
$aItems = array_splice($inventory, 0, $aCount);
$bItems = array();
// iterate over $inventory until a B item is found
foreach($inventory as $index => $item) {
if($item[0] == 'B') {
// extract $bItems from $inventory and shorten $inventory by $bCount
// break out of foreach loop after that
$bItems = array_splice($inventory, $index, $bCount);
break;
}
}
// append $aItems and $bItems to $sets, padd this array with null if
// less then $aCount + $bCount added
$sets[] = array_pad(array_merge($aItems, $bItems), $aCount + $bCount, null);
// if there are still values left in $inventory, call 'makeSets' again
if(count($inventory) > 0) makeSets($sets, $inventory, $aCount, $bCount);
}
$sets = array();
makeSets($sets, $inventory, 1, 2);
print_r($sets);
Since you mentioned that you dont have that much experience with arrays, here are the links to the php documentation for the functions I used in the above code:
array_splice — Remove a portion of the array and replace it with something else
array_merge — Merge one or more arrays
array_pad — Pad array to the specified length with a value
This code sorts inventory without any assumption on inventory ordering. You can specify pattern (in $aPattern), and order is obeyed. It also fills lacking entries with given default value.
<?php
# config
$aInventory=array(
array("A","PINK"),
array("A","MAUVE"),
array("A","ORANGE"),
array("A","GREY"),
array("B","RED"),
array("B","BLUE"),
array("B","YELLOW"),
array("B","GREEN"),
array("B","BLACK"),
array("C","cRED"),
array("C","cBLUE"),
array("C","cYELLOW"),
array("C","cGREEN"),
array("C","cBLACK")
);
$aPattern = array('A','B','A','C');
$mDefault = null;
# preparation
$aCounter = array_count_values($aPattern);
$aCurrentCounter = $aCurrentIndex = array_fill_keys(array_unique($aPattern),0);
$aPositions = array();
$aFill = array();
foreach ($aPattern as $nPosition=>$sElement){
$aPositions[$sElement] = array_keys($aPattern, $sElement);
$aFill[$sElement] = array_fill_keys($aPositions[$sElement], $mDefault);
} // foreach
$nTotalLine = count ($aPattern);
$aResult = array();
# main loop
foreach ($aInventory as $aItem){
$sElement = $aItem[0];
$nNeed = $aCounter[$sElement];
$nHas = $aCurrentCounter[$sElement];
if ($nHas == $nNeed){
$aCurrentIndex[$sElement]++;
$aCurrentCounter[$sElement] = 1;
} else {
$aCurrentCounter[$sElement]++;
} // if
$nCurrentIndex = $aCurrentIndex[$sElement];
if (!isset($aResult[$nCurrentIndex])){
$aResult[$nCurrentIndex] = array();
} // if
$nCurrentPosition = $aPositions[$sElement][$aCurrentCounter[$sElement]-1];
$aResult[$nCurrentIndex][$nCurrentPosition] = $aItem;
} // foreach
foreach ($aResult as &$aLine){
if (count($aLine)<$nTotalLine){
foreach ($aPositions as $sElement=>$aElementPositions){
$nCurrentElements = count(array_keys($aLine,$sElement));
if ($aCounter[$sElement] != $nCurrentElements){
$aLine = $aLine + $aFill[$sElement];
} // if
} // foreach
} // if
ksort($aLine);
# add empty items here
} // foreach
# output
var_dump($aResult);
Generic solution that requires you to specify a pattern of the form
$pattern = array('A','B','B');
The output will be in
$result = array();
The code :
// Convert to associative array
$inv = array();
foreach($inventory as $item)
$inv[$item[0]][] = $item[1];
// Position counters : int -> int
$count = array_fill(0, count($pattern),0);
$out = 0; // Number of counters that are "out" == "too far"
// Progression
while($out < count($count))
{
$elem = array();
// Select and increment corresponding counter
foreach($pattern as $i => $pat)
{
$elem[] = $inv[ $pat ][ $count[$i]++ ];
if($count[$i] == count($inv[$pat]))
$out++;
}
$result[] = $elem;
}

Categories