Parse Relative Day Words For A More Human-Friendly Human Interface

Time is an illusion. System.DateTime, doubly so. *

As software developers, part of our job is to invite those infernal computing machines into our shared hallucination that things in the universe are, in fact, changing in sequence. This means that we have to also streamline the flow of (among other things) time-related information between human brain and digital store. And while mortals have had millennia to work out subtle language for expressing the passage of time and the bookmarks we make on it, computers, having only really been at it for decades, have had a bit of catching up to do.

Even in the friendly, friendly .NET Framework, there are gaps to fill.

To us bipedal time-travelers, the current moment usually holds the most meaning. Consider the way we usually reference a nearby point in time: Yesterday. Tomorrow. Next Tuesday. Justin Bieber.

Okay, not really for that last one. Walk it off.

So, DateTime‘s Parse method is pretty handy with most temporal formats. With relative time English, though, not so much:

datetime-parse

“But dude”, you say, “what can I do to bring my DateTime code up to spec with the expectations of modern upright man?”

I’m Glad You Asked

One blog-post-sized step we can take is to add some code that digests dates in more carbon-friendly formats. Let’s begin with a Parse method that will take just such a now-dependent turn of phrase and return a DateTime instance.

/// <summary>
/// Parses the given date/time string with
/// the ability to recognize relative day words
/// </summary>
public static DateTime? Parse(string input)
{
    // default to null
    DateTime? result;

    // is it yesterday, today or tomorrow?
    result = ParseRelativeDay(input);

    // is it a day of the week?
    if (result == null)
        result = ParseDayOfWeekDate(input);

    // ugh. just parse it with DateTime.
    if (result == null)
    {
        DateTime dt;
        if (DateTime.TryParse(input, out dt))
            result = dt;
    }

    return result;
}

The first thing we should take note of in this function is the order in which it plies its different tricks. Purely relative words are parsed first, because they take precedence linguistically. If today is a Sunday, you wouldn’t call the day before today “Saturday”, you would call it “yesterday”.

If the input doesn’t fall within the set of “today”, “yesterday”, or “tomorrow”, it will try to match the name of a day of the week. If all else fails, we fall quietly through to the safety net of out-of-the-box DateTime parsing.

Now that you’ve got the tune in your head, let’s look closer at the dance steps.

You’re Only A Day Away

For the ParseRelativeDay call, we’ll first need a string, int dictionary to define the values of relative date words. Then we can look up one of those values and add it to DateTime.Today to make it into a usable DateTime instance:

private static Dictionary<string, int> _relativeDayWords;
protected static Dictionary<string, int> RelativeDayWords
{
    get
    {
        if (_relativeDayWords == null)
        {
            _relativeDayWords = new Dictionary<string, int>();
            _relativeDayWords.Add("today", 0);
            _relativeDayWords.Add("yesterday", -1);
            _relativeDayWords.Add("tomorrow", 1);
        }
        return _relativeDayWords;
    }
}
 
public static DateTime? ParseRelativeDay(string input)
{
    DateTime? value = null;
    string key = input.ToLower().Trim();
    if (RelativeDayWords.ContainsKey(key))
        value = DateTime.Today.AddDays(RelativeDayWords[key]);

        return value;
}

Wednesday Thursday Friday

Next, we’ll parry the slightly more complicated affair of picking out day of the week words.

Since day-of-the-week words are, by themselves, ambiguous about whether they describe the future or the past, we should define an enum and some code that will help us sort that out:

public enum Tense
{
    Future,
    Past
}

/// <remarks>
/// TODO: a little uncool to have a GetTense() function 
/// without a corresponding GetLoose() call available. 
/// </remarks>
protected static Tense GetTense(string input)
{
    // default to past (this will vary depending on your app)
    var value = Tense.Past;
    if (input.ToLower().Contains("next"))
        value = Tense.Future;
    else if (input.ToLower().Contains("last"))
        value = Tense.Past;

    return value;
}

Here’s the code to actually parse a day of the week word, whole or partial, and match it to its corresponding System.DayOfWeek value.

protected static DayOfWeek? ParseDayOfWeek(string input)
{
    // cleanse input
    string i = input 
        .ToLower() 
        .Replace("next", string.Empty) 
        .Replace("last", string.Empty) 
        .Trim();

    // query it for a match
    DayOfWeek? result = null;
    var dow = (from p in System.Enum.GetValues(typeof(DayOfWeek)).Cast<DayOfWeek>()
        where p.ToString().ToLower().StartsWith(i)
        select p);

    if (dow.Any())
        result = dow.First();

    return result;
}

The cadence on the end of this verse is, of course, the ParseDayOfWeekDate function itself, which uses the above scaffolding to determine the DayOfWeek and Tense intended by the perhaps imperfect if well-intentioned input string. Using these and a few lines of squinty math, it spits out the expected DateTime.

By the way, do you ever lay awake at night wondering how the fabric of our universe remains stitched together at the edges, containing the chaos, still yet to succumb in a seemingly inevitable fireball of annihilation?

Me neither. Everyone knows it’s held in place by the discrete power of the modulus operator:

/// <summary>
/// Finds the date for the described day of the week.
/// </summary>
public static DateTime? ParseDayOfWeekDate(string input)
{
    DateTime? result = null;
    var day = GetDayOfWeek(input);
    if (day.HasValue)
    {
        var dir = GetTense(input);

        int daysToAdd = 0;
        int currentDayOfWeek = (int)DateTime.Today.DayOfWeek;
        int inputDayOfWeek = (int)day;
        int n;

        switch (dir)
        {
            case Tense.Past:
                n = (inputDayOfWeek - (7 + currentDayOfWeek));
                daysToAdd = (n < -7) ? n % 7 : n;
                break;

            case Tense.Future:
                n = (7 - currentDayOfWeek + inputDayOfWeek);
                daysToAdd = (n > 7) ? n % 7 : n;
                break;
        }

        result = DateTime.Today.AddDays(daysToAdd);
    }

    return result;
}

Bring It Back The Other Way

We are also going to want our date values to be distributed to human eyeballs in just as gentle a manner as that in which they were harvested from the brains behind them. To that end, here’s a ToString function that reverses all of the above operations, rendering a representation of the given DateTime that will resonate in anthropomorphic harmony with the user’s very soul.

/// <summary>
/// Gets a string representing the given date.
/// If the date is within seven days of now, the
/// result is a relative word.
/// </summary>
public static string ToString(DateTime value)
{
    string result = string.Empty;
    // if the date is within 7 days, give it a friendly name
    const int daysPerWeek = 7;
    var difference = DateTime.Today - value;
    if (System.Math.Abs(difference.Days) <= daysPerWeek)
    {
        // relative
        result = (from p in RelativeDayWords
            where DateTime.Today.AddDays(p.Value) == value
            select p.Key).FirstOrDefault();

        // or dow
        if (string.IsNullOrEmpty(result))
        {
            result = value.DayOfWeek.ToString();

            if (value > DateTime.Today)
                result = string.Format("next {0}");
            else
                result = string.Format("last {0}");
        }
    }

    // or native short date format
    if (string.IsNullOrEmpty(result))
        result = value.ToShortDateString();

    return result;
}

I Don’t Want To Date Myself, But

I dropped this little time capsule of .NET science on Tuesday, January 18th, 2011. Here’s a snapshot of this code working to bring man and machine closer:

datetime-snapshot

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s