Comma Quibbling

Comma Quibbling

Rate This

[UPDATE: Holy goodness. Apparently this was a more popular pasttime than I anticipated. There's like a hundred solutions in there. Who knew there were that many ways to stick commas in a string? It will take me some time to go through them all, so don't be surprised if it's a couple of weeks until I get them all sorted out.]

Comma The point of Monday’s post about comma-separated lists was not so much about the actual problem; it’s a rather trivial problem. Rather, I wanted to make two points. First, stating the actual problem rather than a much harder and more general version of the problem is likely to get you a realistic solution to your actual problem much faster. And second, reworking the statement of the problem into an equivalent but structurally different statement is a great way to see solutions that you might have otherwise missed.

But whenever I make a post illustrating such points with a specific example, lots of people pipe up with their ideas for how to solve the specific example. Which is awesome; I encourage this behaviour.

So in that spirit, here’s a slightly harder version of the string concatenation problem, just for the fun of it. Write me a function that takes a non-null IEnumerable<string> and returns a string with the following characteristics:

(1) If the sequence is empty then the resulting string is "{}".
(2) If the sequence is a single item "ABC" then the resulting string is "{ABC}".
(3) If the sequence is the two item sequence "ABC", "DEF" then the resulting string is "{ABC and DEF}".
(4) If the sequence has more than two items, say, "ABC", "DEF", "G", "H" then the resulting string is "{ABC, DEF, G and H}". (Note: no Oxford comma!)

I think you get the idea. You can post your solution in the comments or use the link on the blog page to email your solution to me.

The strings in the sequence can be assumed to be non-null but can otherwise be any string value, including empty strings or strings containing commas, braces and "and".

There’s no size limit on the sequence; it could be tiny, it could be thousands of strings. But it will be finite.

All you get are the methods of IEnumerable<string>; if you want to make that thing into a list or an array, you’re going to need to do that explicitly rather than casting it and hoping for the best.

I am particularly interested in solutions which make the semantics of the code very clear to the code maintainer.

Of course, C# is most interesting to me, but if there are neat ways to express this in other languages, I’d love to see them too.

If there are any particularly amusing or interesting implementations I’ll dissect them on the blog in a future episode, probably in a week or so. I’m not going to have time to do a detailed analysis of every one.

And… go!

  • Attempting to get this to format properly...

    module Enumerable

     def bracketed_english_join

       out = inject([]) { |array, item| array + [item, ', '] }

       '{' +

         case out.length

           when 0 then ''

           when 2 then out[0]

           else (

             out[out.length - 3] = ' and ';

             out[0, out.length - 1].join

           )

         end +

       '}'

     end

    end

    if __FILE__ == $0

     require 'test/unit'

     class BracketedEnglishJoinTestCase < Test::Unit::TestCase

       def test_empty_returns_empty_string

         assert_equal('{}', [].bracketed_english_join)

       end

       def test_single_returns_item_only

         assert_equal(

           '{ABC}',

           ['ABC'].bracketed_english_join

         )

       end

       def test_dual_returns_and_separated

         assert_equal(

           '{ABC and DEF}',

           ['ABC', 'DEF'].bracketed_english_join

         )

       end

       def test_many_returns_comma_then_and_separated

         assert_equal(

           '{ABC, DEF, G and H}',

           ['ABC', 'DEF', 'G', 'H'].bracketed_english_join

         )

       end

     end

    end

  • I'd serialize the input into xsd conforming xml and use xsl like always :). Xsd available upon request...

    <?xml version="1.0" encoding="utf-8"?>

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

       xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">

       <xsl:output method="text" indent="yes"/>

       <xsl:template match="/">

         <xsl:value-of select="'{'"/>

         <xsl:apply-templates select="/root/word"/>

         <xsl:value-of select="'}'"/>

       </xsl:template>

       <xsl:template match="word">

         <xsl:choose>

           <xsl:when test="position() = 1">

             <xsl:value-of select="."/>

           </xsl:when>

           <xsl:when test="position() = last()">

             <xsl:value-of select="concat(' and ', .)"/>

           </xsl:when>

           <xsl:otherwise>

             <xsl:value-of select="concat(' , ', .)"/>

           </xsl:otherwise>

         </xsl:choose>    

       </xsl:template>

    </xsl:stylesheet>

  • I guess this solution has already been posted. .Net 2.0

    Public Function Commate(ByVal Items As IEnumerable(Of String)) As String

           Dim ie As IEnumerator(Of String) = Items.GetEnumerator()

           Dim ls As New List(Of String)

           While ie.MoveNext()

               ls.Add(ie.Current)

           End While

           Dim sb As New System.Text.StringBuilder()

           sb.Append("{")

           If ls.Count > 0 Then

               Dim beforeAnd As String = String.Join(", ", ls.ToArray(), 0, ls.Count - 1)

               If ls.Count > 1 Then

                   sb.Append(String.Join(" and ", New String() {beforeAnd, ls(ls.Count - 1)}))

               Else

                   sb.Append(ls(ls.Count - 1))

               End If

           End If

           sb.Append("}")

           Return sb.ToString()

       End Function

  • The problem is really deciding while enumerating, when you are at the penultimate item in the list, presumably without counting. Here's my solution:

    static class Extensions {        

           public static List<T> IntersperseToList<T>(this IEnumerable<T> values, T delimeter) {

               List<T> res = new List<T>();

               bool first = true;

               foreach (var item in values) {

                   if (!first) {

                       res.Add(delimeter);

                   }

                   res.Add(item);

                   first = false;

               }

               return res;

           }

           public static string Join(this IEnumerable<string> values) {

               return String.Join(String.Empty, values.ToArray());

           }

       }

    static string SolveIt(IEnumerable<string> values) {

               List<string> res = values.IntersperseToList(", ");

               if (res.Count >= 2) {

                   res[res.Count - 2] = " and ";

               }

               return "{" + res.Join() + "}";

           }

           static void Main(string[] args) {

               var vals = Enumerable.Range(1, 10001).Select(i => i.ToString());

               Console.WriteLine(SolveIt(vals));

               Console.WriteLine(SolveIt(new string[0]));

               Console.WriteLine(SolveIt(new string[] { "ABC" }));

               Console.WriteLine(SolveIt(new string[] { "ABC", "DEF" }));

               Console.WriteLine(SolveIt(new string[] { "AA", "BBB", "CC", "H" }));

           }

  • Having read through the various solutions I think Jon nailed it pretty much straight away as far as the actual implementation was concerned.   In terms of clearly expressing the semantic its pretty good too however I was initially confused by the first word to be added to the output being the penultimate word.

    Only having analysed the code more was it clear that all words will pass through penultimate and get added to the the builder before being replaced with tne next candidate to be the penultimate.

    I know the comments kinda indicate this but a small tweak changing the variable name 'penultimate' to 'current' would likely have not lead to that confusion in the first place.

  • class Program

    {

       static void Main()

       {

           AreEqual(Stringify(new string[] {}), "{}");

           AreEqual(Stringify(new [] {"ABC" }), "{ABC}");

           AreEqual(Stringify(new[] { "ABC", "DEF" }), "{ABC and DEF}");

           AreEqual(Stringify(new[] { "ABC", "DEF", "G", "H" }), "{ABC, DEF, G and H}");

       }

       static string Stringify(IEnumerable<string> sequence)

       {

           return "{" +

                       string.Join(", ", sequence.Reverse().Skip(2).Reverse().ToArray()) +

                       (sequence.Count()>2?", ":string.Empty) +

                       string.Join(" and ", sequence.Reverse().Take(2).Reverse().ToArray()) +

                   "}";

       }

       static void AreEqual(string actual, string expected)

       {

           if (actual != expected) throw new Exception(actual + "!=" + expected);

       }

    }

  • #r "FSharp.PowerPack.dll"

    let format (words : seq<string>) =

     let rec format (words : LazyList<string>) acc =

         match words with

             | LazyList.Nil -> string.Empty

             | LazyList.Cons(first, LazyList.Nil) -> first

             | LazyList.Cons(first, LazyList.Cons(second, LazyList.Nil)) -> acc + first + " and " + second

             | LazyList.Cons(first, rest) ->  acc + first + ", " |> format rest

     let listOfWords = LazyList.of_seq words  

     "{" + (format  listOfWords string.Empty) + "}"

    ["ABC"; "DEF"; "G"; "H" ] |> format

    ["ABC"; "DEF" ] |> format

    ["ABC"] |> format

    [] |> format

  • Here's mine!

    public string Format(IList<string> items)

    {

       string separator = items.Count <= 1 ? "" : " and ";

       string allButLast = string.Join(", ", items.TakeWhile((t, i) => i < items.Count - 1).ToArray());

       return string.Format("{{{0}{1}{2}}}", allButLast, separator, items.LastOrDefault());

    }

    public string Format(IEnumerable<string> items)

    {

       return Format(new List<string>(items));

    }

  • Ο Erik Lippert στο τελευταίο του blog post , έθεσε ένα απλό προβληματάκι. Ακολουθεί η λύση που έκανα

  • Looks like I'm a bit late, but here's my F# solution:

    let foo sequence =

      let rec bar ss =

          match ss with

          | [] -> ""

          | [a] -> a

          | [a;b] -> sprintf "%s and %s" a b

          | a::b -> sprintf "%s, %s" a (bar b)

      sprintf "{%s}" (sequence |> List.of_seq |> bar)

    And my C# solution:

    public string foo(IEnumerable<string> sequence)

    {

       var stack = new Stack<string>(sequence);

       string result = "}";

       if (stack.Count > 0)

           result = stack.Pop() + result;

       if (stack.Count > 0)

           result = stack.Pop() + " and " + result;

       while (stack.Count > 0)

           result = stack.Pop() + ", " + result;

       return "{" + result;

    }

  • I made two versions. One is recursive and very simple. The other is using extension methods a bit more, but faily simple to read aswell.

    Code:

     class StringConcatenator

     {

       public static string Concatenate(IEnumerable<string> input)

       {

         return "{" + ConcatenateRecursive(input) + "}";

       }

       private static string ConcatenateRecursive(IEnumerable<string> input)

       {

         switch (input.Count ())

         {

           case 0:

             return string.Empty;

           case 1:

             return input.First ();

           case 2:

             return input.First () + " and " + input.Last ();

           default:

             return input.First () + ", " + ConcatenateRecursive (input.Skip (1));

         }

       }

       public static string Concatenate2(IEnumerable<string> input)

       {

         string result;

         switch (input.Count ())

         {

           case 0:

             result = string.Empty;

             break;

           case 1:

             result = input.First();

             break;

           default:

             result = input.Take (input.Count () - 1)

                           .Aggregate ((n, t) => n + ", " + t)

                           + " and "

                           + input.Last();

             break;

         }

         return "{" + result + "}";

       }

     }

    }

  • I realized my version could be slighty better.

    Here it is, still without use of StringBuilder ;)

    static string Extend(string concat, string separator, string value)

    {

     if ( value == null)

       return concat;

     if ( concat != null)

       return concat + separator + value;

     return value;

    }

    static string Concat(IEnumerable<string> strings)

    {

     string concat = null;

     string lastItem = null;

     foreach(var s in strings)

     {

       concat = Extend(concat, ", ", lastItem);

       lastItem = s;

     }

     concat = Extend(concat," and ", lastItem);

     return "{" + concat + "}";

    }

    // Ryan

  • // My vote: Best is izobr version.

    // See: izobr (April 16, 2009 12:07 AM)

    // Here is little bit refactored izobr version.

    private static IEnumerable<string> ConcatNoOxford(IEnumerable<string> source)

    {

     yield return "{";

     string prevItem = null;   // Use like stack.

     bool hasAnyItem = false;  // Target sequence has any item from source sequence.

     foreach (string item in source)

     {

       //TODO: Check for null/empty.

       if (prevItem != null)

       {

         if (hasAnyItem)

         {

           hasAnyItem = true;

           yield return ", ";

         }

         yield return prevItem;

       }

       prevItem = item;

     }

     if (prevItem != null)

     {

       if (hasAnyItem)

       {

         yield return " and ";

       }

       yield return prevItem;

     }

     yield return "}";            

    }

  •        private string Join( IEnumerable<string> input ) {

               List<string> list = new List<string>( input );

               return SurroundWithBrackets( FormatElements( list ) );

           }

           private static string FormatElements( List<string> list ) {

               if ( list.Count == 0 ) {

                   return  string.Empty;

               }

               if ( list.Count == 1 ) {

                   return list[ 0 ];

               }

               StringBuilder result = new StringBuilder();

               foreach ( string item in AllExceptLastTwo( list ) ) {

                   result.AppendFormat( "{0}, ", item );

               }

               result.Append( FormatLastTwoItems( list ) );

               return result.ToString();

           }

           private static string SurroundWithBrackets( string input ) {

               return string.Format( "{{{0}}}", input );

           }

           private static string FormatLastTwoItems( IList<string> list ) {

               int listCount = list.Count;

               return string.Format( "{0} and {1}", list[ listCount - 2 ], list[ listCount - 1 ] );

           }

           private static IEnumerable<string> AllExceptLastTwo( List<string> list ) {

               //if ( list.Count < 3 ) {

               //    return new string[0];

               //}

               return list.GetRange( 0, list.Count - 2 );

           }

           //Test

           private IEnumerable<string> input;

           private void AssertJoinIs( string exepcted ) {

               string actual = joiner.Join( input );

               Console.WriteLine( "Actual: " + actual );

               Assert.AreEqual( exepcted, actual );

           }

           [Test]

           public void Empty() {

               input = new List<string>();

               AssertJoinIs( "{}" );

           }

           [Test]

           public void SingleElement() {

               input = new string[] {"ABC"};

               AssertJoinIs( "{ABC}");

           }

           [Test]

           public void TwoElements() {

               input = new string[] { "ABC", "DEF" };

               AssertJoinIs( "{ABC and DEF}" );

           }

           [Test]

           public void MoreElements() {

               input = new string[] { "ABC", "DEF", "G", "H" };

               AssertJoinIs( "{ABC, DEF, G and H}" );

           }

  • Eric, could you please do a post about StringBuilder and how the compiler will usually introduce a call to a string.Concat() overload. Many people (even in this thread) seem to have no idea what the compiler is doing and believe they have to use StringBuilder any time they concatenate one string with another.

    If I had a penny for every time in a code review someone has complained that I concatenated strings using '+' instead of using StringBuilder.....

Page 7 of 19 (277 items) «56789»