Reputation: 1
I am trying to reorder columns that are in the CSV file that I am importing in PHP. The order should be ID, Carrier, TrackingNumber, ShippingDate
My code looks like this:
$rename = array('Customer PO#'=>'ID', 'Ship Via'=>'Carrier', 'Tracking Number'=>'TrackingNumber', 'Ship Date'=>'ShippingDate');
$csv = array_map('str_getcsv', file('file.csv'));
foreach($csv[0] as $col => $colname) {
if(!empty($rename[$colname])) $csv[0][$col] = $rename[$colname];
}
array_walk($csv, function(&$a) use ($csv) {
$a = array_combine($csv[0], $a);
if (array_key_exists('Product ID', $a)) {
unset($a['Product ID']);
}
if (array_key_exists('Customer Name', $a)) {
unset($a['Customer Name']);
}
});
var_dump($csv);
My array looks like this:
[1]=>
array(4) {
["ShippingDate"]=>
string(10) "11/21/2018"
["ID"]=>
string(5) "59098"
["Carrier"]=>
string(16) "USPS First Class"
["TrackingNumber"]=>
string(22) "940011020088289578733355"
}
Upvotes: 0
Views: 349
Reputation: 21671
The best way to reorder any associative array is to merge it with a default array. So make a default array like this (in the order you want):
$default = [
'ID' => '',
'Carrier' => '',
'TrackingNumber' => '',
'ShippingDate' => ''
];
Then simply merge the existing data into that array like this:
$default = [
'ID' => '',
'Carrier' => '',
'TrackingNumber' => '',
'ShippingDate' => ''
];
$data = [
'ShippingDate' => '11/27/2018',
'Carrier' => 'abc',
'TrackingNumber' => 'defg',
'ID' => '1',
];
$ordered = array_merge($default, $data);
print_r($ordered);
Bonus1
As a bonus because you already have this array:
$rename = array('Customer PO#'=>'ID', 'Ship Via'=>'Carrier', 'Tracking Number'=>'TrackingNumber', 'Ship Date'=>'ShippingDate');
You can use that to make the default array like this:
$default = array_fill_keys($rename, '');
Which keeps it nice and DRY (Don't repeat yourself)
Bonus2
As another bonus instead of unsetting these 'Product ID'
And 'Customer Name'
You can use array_intersect_key
to remove them:
$rename = array('Customer PO#'=>'ID', 'Ship Via'=>'Carrier', 'Tracking Number'=>'TrackingNumber', 'Ship Date'=>'ShippingDate');
$default = array_fill_keys($rename, '');
$data = [
'ShippingDate' => '11/27/2018',
'Carrier' => 'abc',
'TrackingNumber' => 'defg',
'ID' => '1',
'Product ID' => '123',
'Customer Name' => 'someguy'
];
$ordered = array_merge($default, array_intersect_key($data, $default));
print_r($ordered);
Output
Array
(
[ID] => 1
[Carrier] => abc
[TrackingNumber] => defg
[ShippingDate] => 11/27/2018
)
Basically array_intersect_key
will return all the elements from the first array that have matching keys in the second array. Because we already created our $default
array which is how we want the result mapped out, we can just leverage that for our second array to intersect and remove anything not in the default array.
Pretty simple and elegant.
Other things
One last note, not sure why you are reading the CSV like this:
$csv = array_map('str_getcsv', file('file.csv'));
It's better to use fgetcsv
with a file handle because on large files the file
function will read the entire file into memory, where as fgetcsv
reads the file 1 line at a time then recycles the memory allowing you to process much larger files. If you really need/want a large array with all the CSV data in it at the end, it's quite simple to store it in a new array as you iterate through the file.
Bonus3 - The right/better way
$h = fopen('file.csv', 'f');
// schema [inputKey => outputKey]
$map = ['Customer PO#'=>'ID', 'Ship Via'=>'Carrier', 'Tracking Number'=>'TrackingNumber', 'Ship Date'=>'ShippingDate'];
//create a default or empty array with the keys we want
$default = array_fill_keys($map, '');
$headers = [];
while(!feof($h)){
$data = fgetcsv($h);
//sometimes the last line ends with a \n new line
if(!$data) break;
if(empty($headers)){
//if $headers are empty we haven't set them yet
$headers = $data;
//-- order the map to match the headers in the file --
//merging also patches any holes for headers not in $map
// array combine converts [0=>'Customer PO#'] to ['Customer PO#'=>'Customer PO#']
// array merge replaces the value with the new header from $map if it exists, and it orders $map to match the files order
$map = array_merge(array_combine($headers, $headers), $map);
//bail and go to next line
continue;
}
//merge headers and data (be careful of missing delimiters in the file)
//$map values are the new headers, ordered to match the headers in the file
$data = array_combine($map, $data);
//re-order and remove elements
$mapped = array_merge($default, array_intersect_key($data, $default));
print_r($mapped);
}
Note I can't test this online using a csv file I don't have. That said, we can somewhat test it with some canned data and slight modifications (like changing to foreach instead of while, and dumping the file stuff):
//so for testing purposes only
$canned = [
['Product ID','Customer Name','Ship Via','Tracking Number','Ship Date','Customer PO#'],
['prod', 'cust', 'ship', 'track', 'date', 'po']
];
$map = ['Customer PO#'=>'ID', 'Ship Via'=>'Carrier', 'Tracking Number'=>'TrackingNumber', 'Ship Date'=>'ShippingDate'];
$default = array_fill_keys($map, '');
$headers = [];
foreach($canned as $data){
if(empty($headers)){
$headers = $data;
$map = array_merge(array_combine($headers, $headers), $map);
continue;
}
$data = array_combine($map, $data);
$mapped = array_merge($default, array_intersect_key($data, $default));
print_r($mapped);
}
Output:
Array
(
[ID] => po
[Carrier] => ship
[TrackingNumber] => track
[ShippingDate] => date
)
AS you can see above, the results are ordered by and "filtered" by the $map
array. The keys are in the same order, any "extra" elements are removed. Additionally, if there were missing elements such as items in the $map
but not the file, those would be present with an empty string as the value (because of $default = array_fill_keys(...,'')
). But the big advantage here is if we change the $canned
data order around to something like this (date was moved to the end):
$canned = [
['Product ID','Customer Name','Ship Via','Tracking Number','Customer PO#','Ship Date'],
['prod', 'cust', 'ship', 'track', 'po', 'date']
];
It doesn't affect anything, the result is the same. Which leads me into this:
One big issue that was overlooked is if the CSV order is different then $rename
(in your example, which I renamed to $map
), everything would be broken. This is because there was no correlational between the keys of $renamed
to the actual header in the file.
Maybe your file order is always the same or maybe there is no a first header row (I can't know that without seeing the file) but why chance it when it's easy to account for.
With the same "tricks" I showed in the beginning we can re-order the $map
array to match the headers in the actual file, then use the values of the $map
as our input in array_combine
to rename the headers. Then its a simple matter to remove unwanted data (array_intersect_key()
) and order it based on the $default
array (array_merge
). I should mention we made the $default
array before the loop, which preserved the order before we re-ordered and merged $map
variable to the file.
If you can't tell I work pretty extensively with CSV files...
Lastly
This little bit here bugs me as it indicates you could have headers that are empty, which will be problematic from a standpoint of using them as array keys:
//from you original code
foreach($csv[0] as $col => $colname) {
if(!empty($rename[$colname])) $csv[0][$col] = $rename[$colname];
}
So if you can have empty headers I would suggest adding something similar to this part of my code:
//fix empty headers
while(!feof($h)){
//...
if(empty($headers)){
$headers = $data;
//--new code to fix empty headers
foreach($headers as $key=>&$value){
if(empty($value)) $value = "_empty_$key";
}
//--end new code
$map = array_merge(array_combine($headers, $headers), $map);
continue;
}
//...
}
What this does is update $headers
by reference &$value
and any header with a false value (such as an empty string) will be updated to be _empty_{key}
where {key}
is it's natural array index. So if the first line is like this:
"Product ID",,"Customer Name","Ship Via","Tracking Number","Ship Date","Customer PO#",
Your $headers
will be (after array_combine)
//if we didn't add placeholder we would lose one of the empty headers
//when doing array_combine because array keys must be unique
[
'Product ID' => 'Product ID',
'_empty_1' => '_empty_1',
'Customer Name' => 'Customer Name',
'Ship Via' => 'Ship Via',
'Tracking Number' => 'Tracking Number',
'Ship Date' => 'Ship Date',
'Customer PO#' => 'Customer PO#',
'_empty_7' => '_empty_7'
];
This gives them a unique value, so when they are used as keys you don't lose their place. For example if you had 2 empty headers, and you combine the $headers
(using array_combine
as I showed before) you will lose one of them. There is no need to remove these placeholders because the array_intersect_key
operation will do that anyway. This is also a good place to check and handle duplicate headers if that could be an issue.
Now that I completely re-wrote your code, Enjoy!
Upvotes: 3
Reputation: 57121
To make it flexible enough to allow any sort of re-ordering, I take the $rename
array and then flip (using array_flip()
) it so that the field name become the keys you want. Then just use array_replace()
to overwrite the values from your CSV file...
$input = ["ShippingDate"=>"11/21/2018",
"ID"=> "59098",
"Carrier"=> "USPS First Class",
"TrackingNumber"=> "940011020088289578733355"];
$rename = array('Customer PO#'=>'ID', 'Ship Via'=>'Carrier', 'Tracking Number'=>'TrackingNumber', 'Ship Date'=>'ShippingDate');
$reorder = array_flip($rename);
$output = array_replace($reorder, $input);
print_r( $output );
To link this into the code you already have...
$rename = array('Customer PO#'=>'ID', 'Ship Via'=>'Carrier', 'Tracking Number'=>'TrackingNumber', 'Ship Date'=>'ShippingDate');
$csv = array_map('str_getcsv', file('a.txt'));
$reorder = array_flip($rename);
foreach($csv[0] as $col => $colname) {
if(!empty($rename[$colname])) $csv[0][$col] = $rename[$colname];
}
$csv[0] = array_intersect_key($reorder, $csv[0]);
array_walk($csv, function(&$a) use ($reorder, $csv) {
$a = array_replace($reorder,
array_combine($csv[0], array_intersect_key($reorder, $a)));
});
var_dump($csv);
Upvotes: 1
Reputation: 23958
If that is your array all you need to do is move the ShippingDate as the last item.
Create a temp value of it, unset the array value then add it again to make it the last item.
foreach($arr as $key => $sub){
$temp = $sub['ShippingDate'];
unset($arr[$key]['ShippingDate']);
$arr[$key]['ShippingDate'] = $temp;
}
Or use array_shift:
foreach($arr as &$sub){
$sub['ShippingDate'] = array_shift($sub);
}
Upvotes: 1