parsingdurationnodatime

DurationPattern can't parse without separator


I've been tasked with representing a duration in the format "HHmm". This is at least nominally something a Nodatime.DurationPattern should have no trouble parsing, as it follows the requirements specified here. In fact, it's mentioned specifically as an example of a pattern one could use.

I can't get it to parse correctly without adding some sort of separator between the hours and minutes (e.g. "HH|mm" or "HH!mm" etc will parse w/o issue).

I've written this test code to demonstrate the issue:

    public void ConvertWithPattern()
{
    var result1 = DurationPattern.CreateWithInvariantCulture("HH mm").Parse("12 13");
    var result2 = DurationPattern.CreateWithInvariantCulture("HHmm").Parse("1213");

    Assert.True(result1.Success);
    Assert.Equal(12, result1.Value.Hours);
    Assert.Equal(13, result1.Value.Minutes);
    Assert.True(result2.Success);  // this assertion fails
    Assert.Equal(result1, result2);
}

The failing ParseResult reports shows an UnparsableValueException with the following message:

The value string does not match the required number from the format string "mm". Value being parsed: '1213^'. (^ indicates error position.)

Is there any way I can parse this pattern with a Nodatime.DurationPattern?


Solution

  • Is there any way I can parse this pattern with a Nodatime.DurationPattern?

    Potentially. Just as an explanation, when using a "total value" specifier (like H), the number of letters present is used as a minimum number, not a maximum. It will parse as many digits as it sees (up to 13, anyway). So basically Noda Time is parsing your value of "1213 hours" and then looking for the minutes - that's where it's going wrong.

    If you only need to consider durations in the range 00:00 to 23:59, you can use the "partial" hh specifier instead of HH:

    var pattern = DurationPattern.CreateWithInvariantCulture("hhmm")
    var result = pattern.Parse("1234");
    

    If you need to support more hours than that, it becomes tricky - we'd basically need to parse until there were exactly two characters left for the minutes, and that's not an approach Noda Time supports at the moment.

    If you need to support "more than 23 hours, but only up to 99" (so exactly two digits) then that would at least be feasible with the current architecture, but couldn't easily be represented with existing patterns.

    I've filed an issue about this. I'd appreciate any feedback about precise requirements for this.


    One option which is pretty horrible in terms of both string allocation and potentially reporting errors unhelpfully, but which at least allows you to use IPattern<Duration> until there's a better solution in Noda Time itself, is to implement IPattern<Duration> yourself to insert a separate before parsing, and remove it before formatting. Here's a sample which is at least reasonably functional:

    using NodaTime;
    using NodaTime.Text;
    using System.Text;
    
    public sealed class HHmmDurationPattern : IPattern<Duration>
    {
        private readonly DurationPattern patternWithColons = DurationPattern.CreateWithInvariantCulture("HH:mm");
    
        public StringBuilder AppendFormat(Duration value, StringBuilder builder) =>
            builder.Append(Format(value));
    
        public string Format(Duration value)
        {
            string formatted = patternWithColons.Format(value);
            return formatted.Replace(":", "");
        }
    
        public ParseResult<Duration> Parse(string text)
        {
            // If the input is too short, just let the underlying pattern complain.
            if (text.Length < 2)
            {
                return patternWithColons.Parse(text);
            }
            var textWithColons = text.Substring(0, text.Length - 2) + ":" + text.Substring(text.Length - 2);
            return patternWithColons.Parse(textWithColons);
        }
    }
    

    Note that this does not enforce that there are only 2 digits in the total hours - so it parsed "12345" as "123 hours, 45 minutes". It may not be ideal, but it may be helpful as an interim measure.