views:

1002

answers:

7

Here's a simple problem. I have an application that takes a phone number like "13335557777", and needs to reverse it and insert a dot between each number, like this:

"7.7.7.7.5.5.5.3.3.3.1."

I know I can do this with a StringBuilder and a for-loop to reverse the string and insert the dots, but is there a clever way to do this in LINQ (or some other way)?

Note: for this, I'm not really concerned with performance or memory allocation or whatever, just curious to see how this would be done in LINQ.

+8  A: 

Try this

var source = GetTheString();
var reversed = source.Reverse().Select(x => x.ToString()).Aggregate((x,y) => x + "." + y);

EDIT

This solution is definitely aimed at the "clever" end. It's likely much more performant to use a StringBuilder to build up the string. This solution creates many intermediate strings.

EDIT2

There was some debate about the relative speed of the "clever" solution vs. the StringBuilder approach. I wrote up a quick benchmark to measure the approach. As expected, StringBuilder is faster.

  • Normal Aggregate (100 elements): 00:00:00.0418640
  • WithStringBuilder (100 elements): 00:00:00.0040099
  • Normal Aggregate (1000 elements): 00:00:00.3062040
  • WithStringBuilder (1000 elements): 00:00:00.0405955
  • Normal Aggregate (10000 elements): 00:00:03.0270392
  • WithStringBuilder (10000 elements): 00:00:00.4149977

However, whether or not the speed difference is signficant is highly dependent upon where it is actually used in your application.

Code for the benchmark.

public static class AggregateUnchanged {
    public static string Run(string input) {
        return input
            .Reverse()
            .Select(x => x.ToString())
            .Aggregate((x, y) => x + "." + y);
    }
}

public static class WithStringBuilder {
    public static string Run(string input) {
        var builder = new StringBuilder();
        foreach (var cur in input.Reverse()) {
            builder.Append(cur);
            builder.Append('.');
        }

        if (builder.Length > 0) {
            builder.Length = builder.Length - 1;
        }

        return builder.ToString();
    }
}

class Program {
    public static void RunAndPrint(string name, List<string> inputs, Func<string, string> worker) {

        // Test case. JIT the code and verify it actually works 
        var test = worker("123456");
        if (test != "6.5.4.3.2.1") {
            throw new InvalidOperationException("Bad algorithm");
        }

        var watch = new Stopwatch();
        watch.Start();
        foreach (var cur in inputs) {
            var result = worker(cur);
        }
        watch.Stop();
        Console.WriteLine("{0} ({2} elements): {1}", name, watch.Elapsed, inputs.Count);
    }

    public static string NextInput(Random r) {
        var len = r.Next(1, 1000);
        var builder = new StringBuilder();
        for (int i = 0; i < len; i++) {
            builder.Append(r.Next(0, 9));
        }
        return builder.ToString();
    }

    public static void RunAll(List<string> input) {
        RunAndPrint("Normal Aggregate", input, AggregateUnchanged.Run);
        RunAndPrint("WithStringBuilder", input, WithStringBuilder.Run);
    }

    static void Main(string[] args) {
        var random = new Random((int)DateTime.Now.Ticks);
        RunAll(Enumerable.Range(0, 100).Select(_ => NextInput(random)).ToList());
        RunAll(Enumerable.Range(0, 1000).Select(_ => NextInput(random)).ToList());
        RunAll(Enumerable.Range(0, 10000).Select(_ => NextInput(random)).ToList());
    }
}
JaredPar
For info, while "clever" and "FP", I'm not sure it is good advice... the number of intermediate strings mean that `StringBuilder` would be more pragmatic.
Marc Gravell
@Marc, agreed, they asked for clever though so I felt obligated :)
JaredPar
Yeah, no problem, I did ask for clever at the expense of anything else.
Andy White
I seriously doubt that this will create a performance issue, creating short strings isn't really that expensive. That said, I would suggest using Join instead.
Jonathan Allen
Here's a question: when I have a string in Visual Studio, the "Reverse()" method doesn't show up in intellisense, but it pops up if you start to type it, and it compiles. Why doesn't it show up in intellisense? Is it one of those explicit interface things?
Andy White
No big deal.. but this solution doesn't have the dot at the end like the question :)
markt
That's my bad, the question wasn't really clear.
Andy White
Just a normal string is fine for "7.7.7.7.5.5.5.3.3.3.1.", StringBuilder is overkill
Chris S
@Andy White - the extension methods on string (as `IEnumerable<char>`) are hidden, just to avoid confusion. They are still there, though.
Marc Gravell
@Andy. The short answer is C# Intellisense sucks. The long answer is that it appears to be intentional to keep the method list on String short.
Jonathan Allen
maybe I'll post another question, so that you guys can get credit for these answers!
Andy White
Nevermind, it's already been done! http://stackoverflow.com/questions/345883/why-doesnt-vs-2008-display-extension-methods-in-intellisense-for-string-class
Andy White
@Grauenwolf - really? I've never had reason to complain about it - care to qualify that? Purely out of curiosity...
Marc Gravell
Thanks for the work you did on this, it's an interesting comparison
Andy White
A: 

(removed an answer using a char list)

As per the comment on the other post, my view is that while the LINQ etc may be "clever", it isn't necessarily efficient. It will make a lot of intermediate strings that need to be collected, for example.

I'd stick to StringBuilder etc, unless you have good reason to change it.

Marc Gravell
Why use a StringBuilder when there is String.Join for specifically this purpose?
Jonathan Allen
@Grauenwolf - because string.Join works on a string[], not a char[], so you'd need to first create a load of strings. With StringBuilder you don't need to do that.
Marc Gravell
+1  A: 
        string x = "123456";
        StringBuilder y = new StringBuilder(x.Length * 2);

        for (int i = x.Length - 1; i >= 0; i--)
        {
            y.Append(x[i]);
            y.Append(".");
        }
Nick
"I know I can do this with a StringBuilder and a for-loop to reverse the string and insert the dots, but is there a clever way to do this in LINQ (or some other way)?"
Sean Bright
I was just giving him the code so he didn't have to write it, jerk
Nick
I'm with you, Nick. +1
Marc Gravell
FWIW, you don't need to ".ToString()" One of the Append() overloads takes a char.
Sean Bright
A: 

As long as you're going through an array already, it'd be easier to use string.Join:

string[] source = GetTheStringAsArray();
string reversed = string.Join(".", source.Reverse());
Jacob Proffitt
You skipped the first step, breaking the string into an array or list.
Jonathan Allen
string content = "35557777"; is an array :)
Chris S
Tell me you didn't down vote my answer and then submit your own using essentially the same function...
Jacob Proffitt
A: 
string aString = "13335557777";
string reversed = (from c in aString.Reverse()
                 select c + ".").Aggregate((a, b) => a + b);
markt
A: 

Is this really a linq problem? Just work backwards in a loop per character:

string s = "";
string content = "35557777";
for (int i = content.Length -1; i > 0; i--)
{
    s += content[i] + ".";
}
Console.WriteLine(s);
Console.ReadLine();

If it's a string longer than 4k, use a StringBuilder. Using LINQ for "7.7.7.7.5.5.5.3.3.3.1." is not what LINQ is for, mark me to -99 if you want, and Marc too.

Chris S
Thanks for the answer. I know doing string manip like this is not really a LINQ problem, but this was more of an "algorithm" type question. Maybe it would have been better if I used a list of objects rather than a string.
Andy White
+1  A: 

The benefit of this one is that String.Join is going to be cheaper than ".Aggregate((x,y) => x + "." + y)".

var target = string.Join(".", source.Reverse().Select(c => c.ToString()).ToArray());
Jonathan Allen
Nice, probably the best answer here, imho...
Tracker1