How can I manually parse a custom DateTime format with optional fields in C#

1.3k Views Asked by At

Let's say I have the following dates/times in some in-house almost-ISO-like format:

  • "2011-11-07T11:17"
  • "--T11:17" (11:17 am, no date, only time)
  • "-11-07" (november the 7th, no year, no time)

The separators are mandatory, and enable me to know is a data is present, or not. The data would be set into a structure like:

struct MyDate
{
    int? Year ;
    int? Month ;
    int? Day ;
    int? Hour ;
    int? Minute ;
}

The "easiest" way would be to loop character by character, and extract the data if present.

But I have the nagging impression that there must be some kind of API to extract, for example, an integer and return the index of the first non-integer character (similar to C's strtol).

Is there a strtol-like function in C#, or something more high-level to extract typed data, instead of parsing the string character by character?

4

There are 4 best solutions below

4
On

You can use DateTime.ParseExact. Check documentation here

0
On

IF you know the date format you can use DateTime.ParseExact for example... if you need something similar to strtol you can use Convert.ToInt32 .

4
On

If performance isn't a concern, I'd utilize Regular Expressions for your task.

Use (\d*)-(\d*)-(\d*)T(\d*):(\d*) as an expression, and then parse each capture group into the corresponding field of your structure, converting empty strings to null values:

var match = Regex.Match(str, @"(\d*)-(\d*)-(\d*)T(\d*):(\d*)");
var date = new MyDate();

if (match.Groups[1].Value != "") date.Year = int.Parse(match.Groups[1].Value);
if (match.Groups[2].Value != "") date.Month = int.Parse(match.Groups[2].Value);
if (match.Groups[3].Value != "") date.Day = int.Parse(match.Groups[3].Value);
if (match.Groups[4].Value != "") date.Hour = int.Parse(match.Groups[4].Value);
if (match.Groups[5].Value != "") date.Minute = int.Parse(match.Groups[5].Value);

EDIT: The 'T' and ':' separators are mandatory in this code. If you want it otherwise, refer to @sehe 's answer instead.

1
On

I'd go about it like this:

EDIT 2

Now included:

  • static Parse and TryParse methods in MyDate struct :)
  • a mirroring ToString() for easy testing
  • minimal unit test testcases
  • BONUS demonstration: An implicit conversion operator from string to MyDate (so you can pass a string where MyDate is required)

Again, see it live on http://ideone.com/rukw4

using System;
using System.Text;
using System.Text.RegularExpressions;

struct MyDate 
{ 
    public int? Year, Month, Day, Hour, Minute; 

    private static readonly Regex dtRegex = new Regex(
        @"^(?<year>\d{4})?-(?<month>\d\d)?-(?<day>\d\d)?"
    +   @"(?:T(?<hour>\d\d)?:(?<minute>\d\d)?)?$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

    public static bool TryParse(string input, out MyDate result)
    {
        Match match = dtRegex.Match(input);
        result = default(MyDate);

        if (match.Success)
        {
            if (match.Groups["year"].Success)
                result.Year = Int32.Parse(match.Groups["year"].Value);
            if (match.Groups["month"].Success)
                result.Month = Int32.Parse(match.Groups["month"].Value);
            if (match.Groups["day"].Success)
                result.Day = Int32.Parse(match.Groups["day"].Value);
            if (match.Groups["hour"].Success)
                result.Hour = Int32.Parse(match.Groups["hour"].Value);
            if (match.Groups["minute"].Success)
                result.Minute = Int32.Parse(match.Groups["minute"].Value);
        }

        return match.Success;
    }

    public static MyDate Parse(string input)
    {
        MyDate result;
        if (!TryParse(input, out result))
            throw new ArgumentException(string.Format("Unable to parse MyDate: '{0}'", input));

        return result;
    }

    public override string ToString()
    {
        return string.Format("{0:0000}-{1:00}-{2:00}T{3:00}:{4:00}", Year, Month, Day, Hour, Minute);
    }

    public static implicit operator MyDate(string input)
    {
        return Parse(input);
    }
}

class Program
{
    static void Main(string[] args)
    {
        foreach (var testcase in new [] { 
                "2011-11-07T11:17",
                "-11-07T11:17",
                "2011--07T11:17",
                "2011-11-T11:17",
                "2011-11-07T:17",
                "2011-11-07T11:",
                // extra:
                "--T11:17", // (11:17 am, no date, only time)
                "-11-07", // (november the 7th, no year, no time)
                // failures:
                "2011/11/07 T 11:17",
                "no match" })
        {
            MyDate parsed;
            if (MyDate.TryParse(testcase, out parsed))
                Console.WriteLine("'{0}' -> Parsed into '{1}'", testcase, parsed);
            else
                Console.WriteLine("'{0}' -> Parse failure", testcase);
        }
    }
}

Output:

'2011-11-07T11:17' -> Parsed into '2011-11-07T11:17'
'-11-07T11:17' -> Parsed into '-11-07T11:17'
'2011--07T11:17' -> Parsed into '2011--07T11:17'
'2011-11-T11:17' -> Parsed into '2011-11-T11:17'
'2011-11-07T:17' -> Parsed into '2011-11-07T:17'
'2011-11-07T11:' -> Parsed into '2011-11-07T11:'
'--T11:17' -> Parsed into '--T11:17'
'-11-07' -> Parsed into '-11-07T:'
'2011/11/07 T 11:17' -> Parse failure
'no match' -> Parse failure