Joel Coehoorn
Joel Coehoorn

Reputation: 416039

C# Linq/Lambda expression: How to select an integer out of a string?

I think the best way to explain my question is with a short (generic) linq-to-objects code sample:

IEnumerable<string> ReadLines(string filename)
{
    string line;
    using (var rdr = new StreamReader(filename))
        while ( (line = rdr.ReadLine()) != null)
           yield return line;
}

IEnumerable<int> XValuesFromFile(string filename)
{
    return ReadLines(filename)
               .Select(l => l.Substring(3,3))
               .Where(l => int.TryParse(l))
               .Select(i => int.Parse(i));
}

Notice that this code parses the integer twice. I know I'm missing an obvious simple way to eliminate one of those calls safely (namely because I've done it before). I just can't find it right now. How can I do this?

Upvotes: 4

Views: 8354

Answers (3)

Joel Coehoorn
Joel Coehoorn

Reputation: 416039

I think I'll go with something like this:

IEnumerable<O> Reduce<I,O>(this IEnumerable<I> source, Func<I,Tuple<bool, O>> transform )
{
    foreach (var item in source)
    {
       try
       {
          Result<O> r = transform(item);
          if (r.success) yield return r.value;
       }
       catch {}
    }
}

ReadLines().Reduce(l => { var i; new Tuple<bool, int>(int.TryParse(l.Substring(3,3),i), i)} );

I don't really like this, though, as I'm already on the record as not liking using tuples in this way. Unfortunately, I don't see many alternatives outside of abusing exceptions or restricting it to reference types (where null is defined as a failed conversion), neither of which is much better.

Upvotes: 3

Reed Copsey
Reed Copsey

Reputation: 564631

It's not exactly pretty, but you can do:

return ReadLines(filename)
    .Select(l =>
                {
                    string tmp = l.Substring(3, 3);
                    int result;
                    bool success = int.TryParse(tmp, out result);
                    return new
                               {
                                   Success = success,
                                   Value = result
                               };
                })
    .Where(i => i.Success)
    .Select(i => i.Value);

Granted, this is mostly just pushing the work into the lambda, but it does provide the correct answers, with a single parse (but extra memory allocations).

Upvotes: 3

Marc Gravell
Marc Gravell

Reputation: 1063328

How about:

int? TryParse(string s)
{
    int i;
    return int.TryParse(s, out i) ? (int?)i : (int?)null;
}
IEnumerable<int> XValuesFromFile(string filename)
{
    return from line in ReadLines(filename)
           let start = line.Substring(3,3)
           let parsed = TryParse(start)
           where parsed != null
           select parsed.GetValueOrDefault();
}

You could probably combine the second/third lines if you like:

    return from line in ReadLines(filename)
           let parsed = TryParse(line.Substring(3,3))

The choice of GetValueOrDefault is because this skips the validation check that casting (int) or .Value perform - i.e. it is (ever-so-slightly) faster (and we've already checked that it isn't null).

Upvotes: 9

Related Questions