简体   繁体   中英

Join List<string> Together with Commas Plus "and" for Last Element

I know I could figure a way out but I am wondering if there is a more concise solution. There's always String.Join(", ", lList) and lList.Aggregate((a, b) => a + ", " + b); but I want to add an exception for the last one to have ", and " as its joining string. Does Aggregate() have some index value somewhere I can use? Thanks.

你可以这样做

string finalString = String.Join(", ", myList.ToArray(), 0, myList.Count - 1) + ", and " + myList.LastOrDefault();

Here is a solution which works with empty lists and list with a single item in them:

C#

return list.Count() > 1 ? string.Join(", ", list.Take(list.Count() - 1)) + " and " + list.Last() : list.FirstOrDefault();

VB

Return If(list.Count() > 1, String.Join(", ", list.Take(list.Count() - 1)) + " and " + list.Last(), list.FirstOrDefault())

I use the following extension method (with some code guarding too):

public static string OxbridgeAnd(this IEnumerable<String> collection)
{
    var output = String.Empty;

    var list = collection.ToList();

    if (list.Count > 1)
    {
        var delimited = String.Join(", ", list.Take(list.Count - 1));

        output = String.Concat(delimited, ", and ", list.LastOrDefault());
    }

    return output;
}

Here is a unit test for it:

 [TestClass]
    public class GrammarTest
    {
        [TestMethod]
        public void TestThatResultContainsAnAnd()
        {
            var test = new List<String> { "Manchester", "Chester", "Bolton" };

            var oxbridgeAnd = test.OxbridgeAnd();

            Assert.IsTrue( oxbridgeAnd.Contains(", and"));
        }
    }

EDIT

This code now handles null and a single element:

  public static string OxbridgeAnd(this IEnumerable<string> collection)
    {
        var output = string.Empty;

        if (collection == null) return null;

        var list = collection.ToList();

        if (!list.Any()) return output;

        if (list.Count == 1) return list.First();

        var delimited = string.Join(", ", list.Take(list.Count - 1));

        output = string.Concat(delimited, ", and ", list.LastOrDefault());

        return output;
    }

This version enumerates values once and works with any number of values:

public static string JoinAnd<T>(
    this IEnumerable<T> values, string separator, string sepLast)
{
    if (values == null) throw new ArgumentNullException(nameof(values));

    var sb = new StringBuilder();
    var enumerator = values.GetEnumerator();

    if (enumerator.MoveNext())
    {
        sb.Append(enumerator.Current);
    }

    if (enumerator.MoveNext())
    {
        var obj = enumerator.Current;

        while (enumerator.MoveNext())
        {
            sb.Append(separator);
            sb.Append(obj);
            obj = enumerator.Current;
        }

        sb.Append(sepLast);
        sb.Append(obj);
    }

    return sb.ToString();
}

edit : Fixed bug pointed out in @Artemious's answer to be like string.Join . Added xunit-test:

[Theory]
[InlineData("")]
[InlineData("•")] // • mean null
[InlineData("a")]
[InlineData("ab")]
[InlineData("abc")]
[InlineData("•bc")]
[InlineData("a•c")]
[InlineData("ab•")]
[InlineData("•••")]
public void ResultsAreLikeStringJoinExceptForLastSeparator(string chars)
{
    const string separator = ", ";
    const string lastSeparator = " and ";

    var values = chars.Select(c => c == '•' ? null : c.ToString());

    var actualResult = values.JoinAnd(separator, lastSeparator);

    // Same result as string.Join, but with last ", " replaced with " and ".
    var stringJoined = string.Join(separator, values);
    var stringJoinLike = Regex.Replace(stringJoined, @", (?=\w?$)", lastSeparator);

    Assert.Equal(stringJoinLike, actualResult);
}

This version enumerates values only once and works with any number of values.

(Improved answer of @Grastveit)

I converted it to an extension method and added some unit tests. Added some null-checks. Also I fixed a bug when if an item in the values collection contains null , and it is the last one, it would be skipped at all. This does not correspond to how String.Join() behaves now in the .NET Framework.

#region Usings
using System;
using System.Collections.Generic;
using System.Text;
#endregion 

namespace MyHelpers
{
    public static class StringJoinExtensions
    {
        public static string JoinAnd<T>(this IEnumerable<T> values, 
             string separator, string lastSeparator = null)
        {
            if (values == null)
                throw new ArgumentNullException(nameof(values));
            if (separator == null)
                throw new ArgumentNullException(nameof(separator));

            var sb = new StringBuilder();
            var enumerator = values.GetEnumerator();

            if (enumerator.MoveNext())
                sb.Append(enumerator.Current);

            bool objectIsSet = false;
            object obj = null;
            if (enumerator.MoveNext())
            {
                obj = enumerator.Current;
                objectIsSet = true;
            }

            while (enumerator.MoveNext())
            {
                sb.Append(separator);
                sb.Append(obj);
                obj = enumerator.Current;
                objectIsSet = true;
            }

            if (objectIsSet)
            {
                sb.Append(lastSeparator ?? separator);
                sb.Append(obj);
            }

            return sb.ToString();
        }
    }
}

And here's some unit tests

#region Usings
using MyHelpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Linq;
#endregion

namespace UnitTests
{
    [TestClass]
    public class StringJoinExtensionsFixture
    {
        [DataTestMethod]
        [DataRow("", "", null, null)]
        [DataRow("1", "1", null, null)]
        [DataRow("1 and 2", "1", "2", null)]
        [DataRow("1, 2 and 3", "1", "2", "3")]
        [DataRow(", 2 and 3", "", "2", "3")]
        public void ReturnsCorrectResults(string expectedResult, 
             string string1, string string2, string string3)
        {
            var input = new[] { string1, string2, string3 }
                .Where(r => r != null);
            string actualResult = input.JoinAnd(", ", " and ");
            Assert.AreEqual(expectedResult, actualResult);
        }

        [TestMethod]
        public void ThrowsIfArgumentNulls()
        {
            string[] values = default;
            Assert.ThrowsException<ArgumentNullException>(() =>
                 StringJoinExtensions.JoinAnd(values, ", ", " and "));

            Assert.ThrowsException<ArgumentNullException>(() =>
               StringJoinExtensions.JoinAnd(new[] { "1", "2" }, null, 
                  " and "));
        }

        [TestMethod]
        public void LastSeparatorCanBeNull()
        {
            Assert.AreEqual("1, 2", new[] { "1", "2" }
               .JoinAnd(", ", null), 
                   "lastSeparator is set to null explicitly");
            Assert.AreEqual("1, 2", new[] { "1", "2" }
               .JoinAnd(", "), 
                   "lastSeparator argument is not specified");
        }

        [TestMethod]
        public void SeparatorsCanBeEmpty()
        {
            Assert.AreEqual("1,2", StringJoinExtensions.JoinAnd(
                new[] { "1", "2" }, "", ","), "separator is empty");
            Assert.AreEqual("12", StringJoinExtensions.JoinAnd(
                 new[] { "1", "2" }, ",", ""), "last separator is empty");
            Assert.AreEqual("12", StringJoinExtensions.JoinAnd(
                 new[] { "1", "2" }, "", ""), "both separators are empty");
        }

        [TestMethod]
        public void ValuesCanBeNullOrEmpty()
        {
            Assert.AreEqual("-2", StringJoinExtensions.JoinAnd(
               new[] { "", "2" }, "+", "-"), "1st value is empty");
            Assert.AreEqual("1-", StringJoinExtensions.JoinAnd(
                 new[] { "1", "" }, "+", "-"), "2nd value is empty");
            Assert.AreEqual("1+2-", StringJoinExtensions.JoinAnd(
                new[] { "1", "2", "" }, "+", "-"), "3rd value is empty");

            Assert.AreEqual("-2", StringJoinExtensions.JoinAnd(
             new[] { null, "2" }, "+", "-"), "1st value is null");
            Assert.AreEqual("1-", StringJoinExtensions.JoinAnd(
             new[] { "1", null }, "+", "-"), "2nd value is null");
            Assert.AreEqual("1+2-", StringJoinExtensions.JoinAnd(
             new[] { "1", "2", null }, "+", "-"), "3rd value is null");
        }
    }
}

The simplest way that I can think of is like this... print(', '.join(a[0:-1]) + ', and ' + a[-1])

a = [a, b, c, d]
print(', '.join(a[0:-1]) + ', and ' + a[-1])

a, b, c, and d

Or, if you don't like Canadian syntax, the Oxford comma, and extra squiggles:

print(', '.join(a[0:-1]) + ' and ' + a[-1])

a, b, c and d

Keeping it simple.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM