Question

Given the following source data:

Dim inputs = {New With {.Name="Bob", .Date="201405030500"},
              New With {.Name="Sally", .Date="201412302330"},
              New With {.Name="Invalid", .Date="201430300000"}}

I have written the following LINQ query (with the intention of adding a Where success clause as a filter before the Select, but I've left it out and have included success in the results for debugging purposes):

Dim result1 = From i in inputs
              Let newDate = New DateTime
              Let success = DateTime.TryParseExact(i.Date, "yyyyMMddHHmm", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal, newDate)
              Select New With {
                  .Name = i.Name, 
                  .Success = success,
                  .Date = newDate
              }

However, I get the following results, with the newDate not getting populated by the TryParseExact:

Results without a parsed Date

So, I refactored the query to extract the TryParseExact into an anonymous method to get the following:

Dim parseDate = Function(d As String)
                    Dim parsedDate As DateTime
                    Return New Tuple(Of Boolean, Date)(DateTime.TryParseExact(d, "yyyyMMddHHmm", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal, parsedDate), parsedDate)
                End Function

Dim result2 = From i in inputs
              Let parsedDate = parseDate(i.Date)
              Select New With {
                  .Name = i.Name,
                  .Success = parsedDate.Item1,
                  .Date = parsedDate.Item2
              }

...and this correctly gives me:

Results with a parsed Date

However, I'd like to find a way to do this entirely within the LINQ statement without the need to use an anonymous method to create a tuple. Sure, I've got something that works, but I'd like to do this as a point of academic interest.

I suspect this may be possible, but my question is:

Why doesn't the result1 query properly set newDate with the parsed date?

(I've considered lazy evaluation, but I don't think it applies here.)

Update:

  • Thank you Hamlet for showing me that you can actually declare and invoke an anonymous method at the same time! Mind = Blown!
  • Thank you Jim for suggesting a Nullable instead of a Tuple! Makes a lot of sense.
  • Thank you Ahmad for suggesting the closure, that is certainly cleaner to me than the anonymous method.

Because of the answers below, I've refactored the LINQ as follows (including the Where clause filter):

Dim result3 = From i in inputs
              Let parsedDate = Function(d) 
                                   Dim dtParsed As DateTime
                                   Return If(DateTime.TryParseExact(d, "yyyyMMddHHmm", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal, dtParsed), dtParsed, New DateTime?)
                               End Function(i.Date)
              Where parsedDate.HasValue
              Select New With {
                  .Name = i.Name,
                  .Date = parsedDate.Value
              }

Results with a parsed Date

This achieves what I'm looking for, and I'm trusting the compiler to optimize the heck out of the generated code, but I'd still like to understand exactly why result1 doesn't work. Perhaps that's a question for Eric Lippert or John Skeet.

Was it helpful?

Solution

I suggest you to use TryParsers library which was created especially to use TryParse methods in LINQ queries.

Dim query = From i In inputs
    Let d = TryParsers.TryParse.DateTimeExact(i.Date, "yyyyMMddHHmm", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal)
    Select New With {
          .Name = i.Name,
          .Success = Not d Is Nothing,
          .Date = d.Value
    }

You can not use range variables (let) as ref / out parameters because let keyword creates a read only variable (actually it compiles to a property of anonymous object).

OTHER TIPS

Rather than declare newDate in a Let clause, you can pull it off by declaring newDate outside of the query. Then your query would reference it as you did originally.

Dim newDate As New DateTime
Dim query = From i in inputs
            Let success = DateTime.TryParseExact(i.Date, "yyyyMMddHHmm", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal, newDate)
            Select New With {
              .Name = i.Name, 
              .Success = success,
              .Date = newDate
          }

For what it's worth, in C# we'd need to use the out keyword for the DateTime.TryParseExact method. Attempting to use newDate with a let clause, similar to the first example, would result in the C# compiler generating an error:

Cannot pass the range variable 'newDate' as an out or ref parameter

You may want to consider using Nullable Date for the .Date property and leave the .Success to be an evaluation of the DateTime?.HasValue. You would then need to modify your parseDate lambda to return the nullable as follows:

Dim inputs = {New With {.Name="Bob", .Date="201405030500"},
              New With {.Name="Sally", .Date="201412302330"},
              New With {.Name="Invalid", .Date="201430300000"}}

Dim parseDate = Function(d As String)
                    Dim parsedDate As DateTime? = DateTime.Now
                    If DateTime.TryParseExact(d, "yyyyMMddHHmm", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal, parsedDate) Then
                        Return parsedDate
                    Else
                        Return Nothing
                    End If
                End Function

Dim result2 = From i in inputs
              Select New With {
                  .Name = i.Name,
                  .Date = parseDate(i.Date)
              }

I would actually move the parseDate out from a lambda to static helper method so that you could reuse it easier elsewhere in your code.

Public Shared Function parseDate(d as String) as DateTime?
   Dim parsedDate As DateTime? = DateTime.Now
   If DateTime.TryParseExact(d, "yyyyMMddHHmm", Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal, parsedDate) Then
       Return parsedDate
   Else
       Return Nothing
   End If
End Function
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top