Search code examples
c#stringvalidationstring-formattingdatetime-format

Check if given string is valid format string for converting DateTime


TLDR;

I need to validate whether a given input string is a valid "format string" for parsing DateTime. For example,

  • yy-mm-dd is valid
  • yy-aaaaaaa123 is not valid

I am working on a program which accepts Date format as an input from the user. Below is my stripped out the code

private string datetimeFormat;

public Logger(string dateFormat)
{
    datetimeFormat = dateFormat;
}

...
...
...

// Inside some function
string pretext = $"{DateTime.Now.ToString(datetimeFormat)},{logLevel},";

I need to add validation for the dateFormat string input.

I am thinking of having many possible combinations in an array and accept only those strings. But is there any other way to validate?


Updates:

My input string does not contain any date. This is not a duplicate of the specified question.

This question is not about DateTime at all.


Solution

  • This kind of depends on what you mean by "valid" and how important you think the "only DateTime, nothing else" restriction is to you.

    Here are a few rules that we can use to test format strings, with some express limitations:

    1. Must be suitable for passing to DateTime.ToString(string format) to convert a DateTime value to string.

    2. Must be usable to parse the output of rule 1 into a valid DateTime value.

    3. The output of rule 2 must not contain a time portion.

    4. Optionally, the output of rule 2 should be the same as the input within a defined range of accuracy.

    These will be good enough for many uses, as long as you are expecting that the output be a fully specified date. Year, month and day must be specified. Here's some code to test against those rules:

    static System.Globalization.CultureInfo DateTimeProvider = System.Globalization.CultureInfo.InvariantCulture;
    const System.Globalization.DateTimeStyles ParseExactStyle = System.Globalization.DateTimeStyles.None;
    static DateTime[] DateSamples = new[]
        {
            DateTime.Now,
            DateTime.Today,
            DateTime.Today.AddDays(1 - DateTime.Today.Day),
            DateTime.Parse("10-Jan-2000"),
            DateTime.Parse("01-Oct-1990"),
            DateTime.Parse("13-Feb-1901")
        };
    
    public static bool IsValidDateFormat(string format, out string result)
    {
        var maxDifference = TimeSpan.FromDays(1);
        foreach (var sample in DateSamples)
        {       
            // Rule 1: Must be suitable for '.ToString(...)'
            string sampleString;
            try
            {
                sampleString = sample.ToString(format);
            }
            catch (FormatException e)
            {
                result = $"Failed rule 1: {e.Message}";
                return false;
            }
    
            // Rule 2: Must be able to parse the produced string
            if (!DateTime.TryParseExact(sampleString, format, DateTimeProvider, ParseExactStyle, out var parsed))
            {
                result = $"Failed rule 2: does not parse it's own output. '{sampleString}'";
                return false;
            }
    
            // Rule 3: No time values.
            if (parsed != parsed.Date)
            {
                result = $"Failed rule 3: No time values. '{sampleString}' => #{parsed}#";
                return false;
            }
    
            // Rule 4: Difference must be less than maxDifference
            TimeSpan difference = sample < parsed ? parsed - sample : sample - parsed;
            if (difference >= maxDifference)
            {
                result = $"Failed rule 4: difference '{difference}' too large.";
                return false;
            }
        }
    
        result = "OK";
        return true;
    }
    

    (This sets the result out parameter to a description of why the format string failed, or OK if it passed, but you might prefer to return a simple enum value.)

    This validates on all sorts of weird formats, including those with extra non-contextual - or at least non-time - characters. The samples include a few tests against time values, order reversal and so on.

    There are some limitations however:

    • TryParseExact does not work with standard format strings like d, 'F', etc.
    • Nor does it work with the 3+ digit year format (yyy) and other stretching formats.
    • The samples include a test that prevents 2-digit years being used.

    In short, it's good enough for simple work. You can trim it back a bit.