90

This array holds a list of items, and I want to turn it into a string, but I don't know how to make the last item have a &/and before it instead of a comma.

1 => coke 2=> sprite 3=> fanta

should become

coke, sprite and fanta

This is the regular implode function:

$listString = implode(', ', $listArrau);

What's an easy way to do it?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
lisovaccaro
  • 32,502
  • 98
  • 258
  • 410
  • 4
    Why not cut off the last element of the array before imploding the rest? And then just .= concenate to your string. – djot Dec 21 '11 at 07:06

16 Answers16

136

A long-liner that works with any number of items:

echo join(' and ', array_filter(array_merge(array(join(', ', array_slice($array, 0, -1))), array_slice($array, -1)), 'strlen'));

Or, if you really prefer the verboseness:

$last  = array_slice($array, -1);
$first = join(', ', array_slice($array, 0, -1));
$both  = array_filter(array_merge(array($first), $last), 'strlen');
echo join(' and ', $both);

The point is that this slicing, merging, filtering and joining handles all cases, including 0, 1 and 2 items, correctly without extra if..else statements. And it happens to be collapsible into a one-liner.

deceze
  • 510,633
  • 85
  • 743
  • 889
  • 23
    +1 for getting it all one one line, -1 for having to come back 5 months from now and figure out what it's trying to do. – Mike Purcell Dec 21 '11 at 07:19
  • 3
    +1 for cleverness, but not the most readable solution in the world :P – Interrobang Dec 21 '11 at 07:20
  • 4
    Oh, feel free to break that up into individual steps if you want to, it's just that this way of doing it doesn't require any `ifs` or loops and can all be done in one expression. That's specifically why I'm calling this a *long-liner*. And of course you'd put this in a function called `joinWithLastSeparator(array $array, $separator = ' and ')`, so that you know what it does 5 months down the road. :o) – deceze Dec 21 '11 at 07:39
  • 1
    @Col Now that's entirely in the eye of the beholder, isn't it. I find superfluous control structures to be much more of an eyesore. You can easily break this apart into several statements if the long line is not to your taste. The logic is about as minimal as it gets though. – deceze Feb 16 '12 at 23:04
  • @Nin Because you insist... see update. Performance really shouldn't be a big problem, there isn't a lot going on here. – deceze Sep 21 '12 at 14:03
  • 1
    This solution has a serious flaw: Because of array_filter(), the last element is omitted if it equals false (such as "0", which is a perfectly valid string to include in a list). If there are only two elements, and they both equal false, the empty string is returned. – Magnar Myrtveit Sep 24 '15 at 21:50
  • 1
    @Magnar You're right, thanks for catching that. I've used a more explicit filter which handles this case properly now. – deceze Sep 25 '15 at 07:46
  • 2
    just seen this post because of a duplicate and got a slightly dirtier but also slighly shorter line. thought i just throw that in here `echo implode(" and ", array_filter(array_reverse([array_pop($array), implode(", ", $array)])));` – Franz Gleichmann Feb 17 '16 at 07:27
  • 6
    Five years later, and I do have to admit that I would probably downvote my own answer... I'm still proud of the fact that it's a single expression, but... hum, yeah... ;o) – deceze Apr 14 '16 at 13:28
  • 2
    Nah, don't listen to the one-liner haters. All those complaints are easily handled by abstracting the calls out to a well-named function. `StringUtils::implodeWithPrefixOnLast($array, ', ', ' and ');` – Nathan May 22 '16 at 09:46
  • This almost works for the Oxford comma (but not for two elements). – Peter Mortensen Jul 06 '19 at 21:37
114

I'm not sure that a one liner is the most elegant solution to this problem.

I wrote this a while ago and drop it in as required:

/**
 * Join a string with a natural language conjunction at the end. 
 * https://gist.github.com/angry-dan/e01b8712d6538510dd9c
 */
function natural_language_join(array $list, $conjunction = 'and') {
  $last = array_pop($list);
  if ($list) {
    return implode(', ', $list) . ' ' . $conjunction . ' ' . $last;
  }
  return $last;
}

You don't have to use "and" as your join string, it's efficient and works with anything from 0 to an unlimited number of items:

// null
var_dump(natural_language_join(array()));
// string 'one'
var_dump(natural_language_join(array('one')));
// string 'one and two'
var_dump(natural_language_join(array('one', 'two')));
// string 'one, two and three'
var_dump(natural_language_join(array('one', 'two', 'three')));
// string 'one, two, three or four'
var_dump(natural_language_join(array('one', 'two', 'three', 'four'), 'or'));

It's easy to modify to include an Oxford comma if you want:

function natural_language_join( array $list, $conjunction = 'and' ) : string {
    $oxford_separator = count( $list ) == 2 ? ' ' : ', ';
    $last = array_pop( $list );

    if ( $list ) {
        return implode( ', ', $list ) . $oxford_separator . $conjunction . ' ' . $last;
    }

    return $last;
}
Ian Dunn
  • 3,541
  • 6
  • 26
  • 44
Angry Dan
  • 3,241
  • 2
  • 21
  • 18
  • +1 for that solution. Readable and good. However returning null is a bit odd to me. I prefer to return empty string if the array is having no elements. – Mitko Delibaltov Apr 25 '18 at 08:02
31

You can pop last item and then join it with the text:

$yourArray = ('a', 'b', 'c');
$lastItem = array_pop($yourArray); // c
$text = implode(', ', $yourArray); // a, b
$text .= ' and '.$lastItem; // a, b and c
JercSi
  • 1,037
  • 1
  • 9
  • 19
  • 1
    Also it does not work with sizeof($yourArray) == 0. Then the correct solution will be, to check first the length of the array and then execute the code above. if size==0, then do nothing; if size==1, return first item; else execute the code above. – JercSi Dec 21 '11 at 07:40
  • 7
    It works like a charm when adding a condition `if (count($yourArray) > 1)`... Thanks :) – Enissay Aug 07 '13 at 21:50
  • Enrique's solution works for 1 element (and 0 elements) without extra 'if's. – ChrisV Apr 02 '14 at 10:30
14

Try this:

$str = array_pop($array);
if ($array)
    $str = implode(', ', $array)." and ".$str;
Enrique
  • 199
  • 1
  • 3
  • 7
  • 1
    The only problem with this one is if I want a comma before the final element if there are three or more elements but I might only have two elements in some instances, which would add an errant comma if using the same code. This will work in 99% of situations though I think, so +1. – Eckstein Aug 27 '14 at 04:22
4

Another possible short solution:

$values = array('coke', 'sprite', 'fanta');

$values[] = implode(' and ', array_splice($values, -2));
print implode(', ', $values);  // "coke, sprite and fanta"

It works fine with any number of values.

VisioN
  • 143,310
  • 32
  • 282
  • 281
3

My go-to, similar to Enrique's answer, but optionally handles the oxford comma.

public static function listifyArray($array,$conjunction='and',$oxford=true) {
    $last = array_pop($array);
    $remaining = count($array);
    return ($remaining ?
        implode(', ',$array) . (($oxford && $remaining > 1) ? ',' : '') . " $conjunction "
        : '') . $last;
}
JJ Wright
  • 31
  • 4
1

I know im way to late for the answer, but surely this is a better way of doing it?

$list = array('breakfast', 'lunch', 'dinner');
$list[count($list)-1] = "and " . $list[count($list)-1];
echo implode(', ', $list);
Jack B
  • 547
  • 4
  • 22
  • 1
    Has the disadvantage of always having a comma before the final element (before the 'and') – ChrisV Apr 02 '14 at 10:26
  • It's still correct grammar. However, I agree with your point. Enrique's answers seems to be similar to mine but solves this problem :) – Jack B Apr 02 '14 at 13:49
  • Works when the list has more than one item, but not when the list is dynamic. (Easily solved with an `if` wrapper) – random_user_name Oct 15 '14 at 18:51
1

This can be done with array_fill and array_map. It is also a one-liner (seems that many enjoy them)), but formated for readability:

$string = implode(array_map(
    function ($item, $glue) { return $item . $glue; }, 
    $array,
    array_slice(array_fill(0, count($array), ', ') + ['last' => ' and '], 2)
));

Not the most optimal solution, but nevertheless.

Here is the demo.

sevavietl
  • 3,762
  • 1
  • 14
  • 21
0

I just coded this based on the suggestions on this page. I left in my pseudo-code in the comments in case anyone needed it. My code differs from others here as it handles different-sized arrays differently and uses the Oxford comma notation for lists of three or more.

    /**
     * Create a comma separated list of items using the Oxford comma notation.  A
     * single item returns just that item.  2 array elements returns the items
     * separated by "and".  3 or more items return the comma separated list.
     *
     * @param array $items Array of strings to list
     * @return string List of items joined by comma using Oxford comma notation
     */
    function _createOxfordCommaList($items) {
        if (count($items) == 1) {
            // return the single name
            return array_pop($items);
        }
        elseif (count($items) == 2) {
            // return array joined with "and"
            return implode(" and ", $items);
        }
        else {
            // pull of the last item
            $last = array_pop($items);

            // join remaining list with commas
            $list = implode(", ", $items);

            // add the last item back using ", and"
            $list .= ", and " . $last;

            return $list;
        }
    }
zkent
  • 980
  • 1
  • 10
  • 22
0

This is quite old at this point, but I figured it can't hurt to add my solution to the pile. It's a bit more code than other solutions, but I'm okay with that.

I wanted something with a bit of flexibility, so I created a utility method that allows for setting what the final separator should be (so you could use an ampersand, for instance) and whether or not to use an Oxford comma. It also properly handles lists with 0, 1, and 2 items (something quite a few of the answers here do not do)

$androidVersions = ['Donut', 'Eclair', 'Froyo', 'Gingerbread', 'Honeycomb', 'Ice Cream Sandwich', 'Jellybean', 'Kit Kat', 'Lollipop', 'Marshmallow'];

echo joinListWithFinalSeparator(array_slice($androidVersions, 0, 1)); // Donut
echo joinListWithFinalSeparator(array_slice($androidVersions, 0, 2)); // Donut and Eclair
echo joinListWithFinalSeparator($androidVersions); // Donut, Eclair, Froyo, Gingerbread, Honeycomb, Ice Cream Sandwich, Jellybean, Kit Kat, Lollipop, and Marshmallow
echo joinListWithFinalSeparator($androidVersions, '&', false); // Donut, Eclair, Froyo, Gingerbread, Honeycomb, Ice Cream Sandwich, Jellybean, Kit Kat, Lollipop & Marshmallow

function joinListWithFinalSeparator(array $arr, $lastSeparator = 'and', $oxfordComma = true) {
    if (count($arr) > 1) {
        return sprintf(
            '%s%s %s %s', 
            implode(', ', array_slice($arr, 0, -1)),
            $oxfordComma && count($arr) > 2 ? ',':'',
            $lastSeparator ?: '', 
            array_pop($arr)
        );
    }

    // not a fan of this, but it's the simplest way to return a string from an array of 0-1 items without warnings
    return implode('', $arr);
}
Josh
  • 8,079
  • 3
  • 24
  • 49
0

OK, so this is getting pretty old, but I have to say I reckon most of the answers are very inefficient with multiple implodes or array merges and stuff like that, all far more complex than necessary IMO.

Why not just:

implode(',', array_slice($array, 0, -1)) . ' and ' . array_slice($array, -1)[0]
greenbutterfly
  • 323
  • 2
  • 9
0

Simple human_implode using regex.

function human_implode($glue = ",", $last = "y", $elements = array(), $filter = null){
    if ($filter) {
        $elements = array_map($filter, $elements);
    }

    $str = implode("{$glue} ", $elements);

    if (count($elements) == 2) {
        return str_replace("{$glue} ", " {$last} ", $str);
    }

   return preg_replace("/[{$glue}](?!.*[{$glue}])/", " {$last}", $str);
}

print_r(human_implode(",", "and", ["Joe","Hugh", "Jack"])); // => Joe, Hugh and Jack
uruapanmexicansong
  • 3,068
  • 1
  • 18
  • 22
0

Another, although slightly more verbose, solution I came up with. In my situation, I wanted to make the words in the array plural, so this will add an "s" to the end of each item (unless the word already ends in 's':

$models = array("F150","Express","CR-V","Rav4","Silverado");
foreach($models as $k=>$model){ 
    echo $model;
    if(!preg_match("/s|S$/",$model)) 
        echo 's'; // add S to end (if it doesn't already end in S)
    if(isset($models[$k+1])) { // if there is another after this one.
        echo ", "; 
        if(!isset($models[$k+2])) 
            echo "and "; // If this is next-to-last, add  ", and" 
        }
    }
}

outputs:

F150s, Express, CR-Vs, Rav4s, and Silverados
Alex Russell
  • 172
  • 1
  • 13
0

Try this,

<?php
$listArray = array("coke","sprite","fanta");

foreach($listArray as $key => $value)
{
 if(count($listArray)-1 == $key)
  echo "and " . $value;
 else if(count($listArray)-2 == $key)
  echo $value . " ";
 else
  echo $value . ", ";
}
?>
planet x
  • 1,599
  • 5
  • 26
  • 44
0

try this

$arr = Array("coke","sprite","fanta");
$str = "";
$lenArr = sizeof($arr);
for($i=0; $i<$lenArr; $i++)
{
    if($i==0)
        $str .= $arr[$i];
    else if($i==($lenArr-1))
        $str .= " and ".$arr[$i];
    else
        $str .= " , ".$arr[$i];
}
print_r($str);
Astha
  • 1,728
  • 5
  • 17
  • 36
-2

It's faster then deceze's solution and works with huge arrays (1M+ elements). The only flaw of both solutions is a poor interaction with a number 0 in a less then three elements arrays becouse of array_filter use.

echo implode(' and ', array_filter(array_reverse(array_merge(array(array_pop($array)), array(implode(', ',$array))))));
Błażej Klisz
  • 1,730
  • 2
  • 16
  • 16