DateTime DSL: Finishing Out the DSL

01.25.2012 09:03 by kbeckman | Comments

C4SC DSL Series Catalog

GitHub Repo

 

Well, Readers… I’m proud to say that the C4SC DateTime DSL is finally complete (as far as this blog series is concerned anyway). Below I detail some changes that I mentioned in my prior post as well as a few that hadn’t thought of at the time. With 170+ unit tests covering the various pieces of DSL functionality, this is finally ready for production! Check out the complete changes in their entirety in my C4SC GitHub Repo.

 

Adding the Week() and Weeks() Methods…

This enhancement was extremely easy considering that the necessary DSL plumbing was already there. Adding the Week() and Weeks() methods simply required multiplying the Day component of the DateTimeComponents instance by 7. I’ll spare you the unit test updates as they don’t look any different than before. They just account for any additional days added or subtracted by Week() or Weeks().

/// <summary>
/// Converts an Int32 to its equivalent DateTimeComponents representation in weeks. 
/// </summary>
/// <param name="weeks">Number of weeks.</param>
/// <returns><see cref="DateTimeComponents"/> composed of the given week parameter.</returns>
public static DateTimeComponents Week(this Int32 weeks)
{
    return weeks.Weeks();
}

/// <summary>
/// Converts an Int32 to its equivalent DateTimeComponents representation in weeks. 
/// </summary>
/// <param name="weeks">Number of weeks.</param>
/// <returns><see cref="DateTimeComponents"/> composed of the given weeks parameter.</returns>
public static DateTimeComponents Weeks(this Int32 weeks)
{
    return new DateTimeComponents(0, 0, weeks * 7, 0, 0, 0);
}

 

Yesterday and Tomorrow…

Implementing the DSL equivalent of Yesterday and Tomorrow is definitely the portion of the DSL that I’m least proud of. The static nature of C# required me to write way more code than I wanted to pull this off. Having a language feature like modules would have made this a trivial exercise as I would have been able to include a globally available set of methods for Yesterday and Tomorrow. Instead, I created a static class for both Yesterday and Tomorrow each with a Date() method that returns the desired DateTime value. A whole class you say? Yeah I know… It was the only way I could think of though to achieve the syntax that I wanted… I guess it’s a small price to pay for better code readability. The DateTimeNowAdapter noise you see below is explained here for those of you just joining in.

/// <summary>
/// DateTime DSL implementation for Yesterday's date.
/// </summary>
public static class Yesterday
{
    private static IDateTimeNowAdapter _nowAdapter = new SystemDateTimeNowAdapter();
    private static readonly object _syncLock = new object();

    /// <summary>
    /// Sets the DSL's IDateTimeNowAdapter. This is the injection point for tests to provide their own adapter
    /// implementation.
    /// </summary>
    /// <param name="adapter"><see cref="IDateTimeNowAdapter"/> implementation.</param>
    internal static void SetDateTimeNowAdapter(IDateTimeNowAdapter adapter)
    {
        lock (_syncLock) { _nowAdapter = adapter; }
    }

    /// <summary>
    /// Yesterday's Date.
    /// </summary>
    /// <returns>Yesterday's date value 24 hours ago.</returns>
    public static DateTime Date()
    {
        return _nowAdapter.DateTimeNow().Date.AddDays(-1);
    }
}

 

Saving the Best For Last… The [Almost] Complete .NET Port of the Rails Date Class

Throughout this series, I’ve commented that the inspiration for this DSL was syntax I encountered in the Ruby language – more specifically the helper methods in the Rails Date class. All of the work up to this point for calculating DateTime covered only a portion of the methods and functionality available in Rails’ Date class. But in the spirit of open source, I wanted to finish this DSL out for anyone besides me who might want to use it.  Instead of providing a set of methods that act upon a semantic model to cache a set of calculations I wanted to make on a DateTime value, I was simply able to add some extension methods to the native .NET DateTime Type. For brevity, only a partial class implementation is below.

 

Anyone who is familiar with the Rails Date class will notice some missing methods… Luckily for me, the functionality in the missing methods is available as part of the native .NET DateTime or String Types and didn’t need to be accounted for here. These are things like to_datetime (DateTime.Parse()), to_formatted_s (String.Format), acts_like_date?, etc. There are also a lot of method aliases that I left out in favor of a single method syntax for a given piece of functionality. For my .NET readers, Ruby aliases are just another name for method delegation where you’d wrap an existing method call with another method with a better or different name.

/// <summary>
/// DateTime extension methods. This class is a .NET partial port of the helper methods in the Rails Date class.
/// </summary>
/// <remarks>
/// http://api.rubyonrails.org/classes/Date.html
/// </remarks>
public static class DateTimeExtensions
{
    private static readonly int[] _q1 = new[] { 1, 2, 3 };
    private static readonly int[] _q2 = new[] { 4, 5, 6 };
    private static readonly int[] _q3 = new[] { 7, 8, 9 };

    /// <summary>
    ///    Resets the time portion of the DateTime to 00:00:00.
    /// </summary>
    /// <param name="dateTime">DateTime evaluation target.</param>
    /// <returns>Given date value at 00:00:00.</returns>
    public static DateTime AtMidnight(this DateTime dateTime)
    {
        return dateTime.AtBeginningOfDay();
    }

    /// <summary>
    ///    Resets the time portion of the DateTime to 00:00:00.
    /// </summary>
    /// <param name="dateTime">DateTime evaluation target.</param>
    /// <returns>Given date value at 00:00:00.</returns>
    public static DateTime AtBeginningOfDay(this DateTime dateTime)
    {
        return dateTime.Date;
    }

    /// <summary>
    ///    Resets the day value to Sunday of the current week at 00:00:00.
    /// </summary>
    /// <param name="dateTime">DateTime evaluation target.</param>
    /// <returns>Sunday of the current week at 00:00:00.</returns>
    public static DateTime AtBeginningOfWeek(this DateTime dateTime)
    {
        return dateTime.AddDays(-(Int32)dateTime.DayOfWeek).AtBeginningOfDay();
    }

    /// <summary>
    ///    Resets the day value to the first of the month at 00:00:00.
    /// </summary>
    /// <param name="dateTime">DateTime evaluation target.</param>
        /// <returns>First day of the month at 00:00:00.</returns>
    public static DateTime AtBeginningOfMonth(this DateTime dateTime)
    {
        return new DateTime(dateTime.Year, dateTime.Month, 1);
    }

    /// <summary>
    ///    Resets the date value to the first day of the quarter at 00:00:00.
    /// </summary>
    /// <param name="dateTime">DateTime evaluation target.</param>
    /// <returns>First day of the quarter at 00:00:00.</returns>
    public static DateTime AtBeginningOfQuarter(this DateTime dateTime)
    {
        return _q1.Contains(dateTime.Month) ? new DateTime(dateTime.Year, 1, 1).AtBeginningOfDay() :
            _q2.Contains(dateTime.Month) ? new DateTime(dateTime.Year, 3, 1).AtBeginningOfDay() :
            _q3.Contains(dateTime.Month) ? new DateTime(dateTime.Year, 7, 1).AtBeginningOfDay() :
                new DateTime(dateTime.Year, 10, 1).AtBeginningOfDay();
    }

    /// <summary>
    ///    Resets the day value to January 1 of the current year at 00:00:00.
    /// </summary>
    /// <param name="dateTime">DateTime evaluation target.</param>
    /// <returns>January 1 of the current year at 00:00:00.</returns>
    public static DateTime AtBeginningOfYear(this DateTime dateTime)
    {
        return new DateTime(dateTime.Year, 1, 1);
    }
}

 

A Quick Moment To Reflect…

This series was a LOT of fun and incredibly educational for me. It allowed me to take my new appreciation for more English-readable code from my recent work with Ruby and open source projects and apply it to a static language in C#. I’ll be taking these new techniques forward with me in future .NET development and I won’t be one to settle with the syntax offered by the .NET Framework or other frameworks. I hope you enjoyed this as much as I did!

 

Comments welcome!

DateTime DSL: Method Chaining

01.08.2012 08:54 by kbeckman | Comments

C4SC DSL Series Catalog

GitHub Repo

 

Continuing on with the DateTime DSL development… It’s time to add some method chaining DateTimeComponents to make the DSL even more robust.

 

Looking Ahead to the Desired DSL Syntax…

Until now, if you’ve been following along the with this series, the DateTime DSL syntax has been pretty clean. And by “pretty clean” I mean, that the parentheses usage has been kept at a minimum. Now that I’ll be implementing chaining on the DateTimeComponents struct, there’s going to be a few more parentheses than I’d prefer. But hey, we’re not using Ruby… In Ruby, parentheses are really only required when you need them for better readability. As you’ll see below, the chaining introduces consecutive parentheses because of the argument required by the And() method. I wish there was a way to avoid it here, but sometimes you just have to accept the limitations of the language you’re using…

DateTime lunchTime     = 3.Hours().And(45.Minutes()).FromNow();
DateTime endOfWorld    = 11.Months().And(21.Days()).From(newYearsEve2011);
DateTime openingBell   = 6.Hours().And(30.Minutes()).AgoFrom(closingBell);
DateTime raceStart     = 3.Minutes().And(42.Seconds()).Ago();

 

The And() Method

As far as the DSL is concerned, we get an incredible amount of additional functionality from And(). With the addition of this single extension method to DateTimeComponents, the DSL now offers the functionality to modify all elements of DateTime before the conversion to the DateTime Type. It accomplishes this by returning a new instance of DateTimeComponents where all properties are summed from the participating DateTimeComponents operands. The And() method gives you unlimited chaining on DateTimeComponents (unless there’s some language/compiler limitation that I’m not aware of in C#) allowing for essentially any combination. However, for readability’s sake, I would suggest a limit of two or three. The code will simply read better and it will sound a lot more like proper, spoken English. You’ll see from the unit tests below that more than two or three usages of And() gets a little wordy.

/// <summary>
/// Extension methods to support method-chaining on DateTimeComponents allowing for modification of all components
/// of date and time before the actual conversion to <see cref="DateTime"/>.
/// </summary>
public static class DateTimeComponentsChaining
{
    public static DateTimeComponents And(this DateTimeComponents components, DateTimeComponents operand)
    {
        return new DateTimeComponents(components.Years + operand.Years
                                    , components.Months + operand.Months
                                    , components.Days + operand.Days
                                    , components.Minutes + operand.Minutes
                                    , components.Seconds + operand.Seconds
                                    , components.Milliseconds + operand.Milliseconds);
    }
}

 

The Unit Tests

And voila! Chaining! For brevity, I’ve only included the addition tests that calculate the DateTime FromNow(). The addition tests that calculate DateTime from a given DateTime (using the From() method) as well as all of the relevant subtraction tests (using the Ago() and AgoFrom() methods) look almost exactly the same. For those of you just joining in who aren’t aware of the _nowAdapter usage below, please refer to the last post on stubbing static dependencies.

[Test]
public void it_should_add_1_of_each_component_to_now()
{
    DateTime actual = 1.Year()
                       .And(1.Month())
                       .And(1.Day())
                       .And(1.Minute())
                       .And(1.Second())
                       .And(1.Millisecond()).FromNow();

    DateTime expected = _nowAdapter.DateTimeNow().AddYears(1)
                            .AddMonths(1)
                            .AddDays(1)
                            .AddMinutes(1)
                            .AddSeconds(1)
                            .AddMilliseconds(1);

    Assert.That(actual, Is.EqualTo(expected));
}

[Test]
[TestCase(1, 2, 3, 4, 5, 6)]
[TestCase(2, 4, 6, 8, 10, 12)]
[TestCase(5, 10, 15, 20, 25, 30)]
[TestCase(100, 200, 300, 400, 500, 600)]
public void it_should_add_all_components_to_now(Int32 years, Int32 months, Int32 days, Int32 minutes, Int32 seconds, Int32 milliseconds)
{
    DateTime actual = years.Years()
                        .And(months.Months())
                        .And(days.Days())
                        .And(minutes.Minutes())
                        .And(seconds.Seconds())
                        .And(milliseconds.Milliseconds()).FromNow();

    DateTime expected = _nowAdapter.DateTimeNow().AddYears(years)
                            .AddMonths(months)
                            .AddDays(days)
                            .AddMinutes(minutes)
                            .AddSeconds(seconds)
                            .AddMilliseconds(milliseconds);

    Assert.That(actual, Is.EqualTo(expected));
}

 

Wrap-Up

We’re nearing the end of this C4SC series, but there is at least a couple more things I’d like to address. I’m not sure if I’ll have one or two more posts, but here’s a sneak peak at some of the things I’ll be addressing…

  • I think the DSL needs some helper methods for common DateTime-related spoken English. In other words, I want a few helpers like Yesterday(),Tomorrow(), etc.; and maybe an addition to the Int32ConversionToDateTimeComponents extension methods for Week(); and possibly a few others… I’ve really been thinking about this a lot over the last few days. With a language feature like modules (i.e. Ruby and VB.NET), I could easily pull off the Yesterday() and Tomorrow() methods. However, since this is all in C#, they’re probably going to be static methods on a static class. Not a big deal at all functionality-wise, but it’s going to take some thought (and maybe a trip or two to the thesaurus) to make the DSL syntax accommodate this.
  • I keep thinking there’s a better way to handle the unit test organization, but I haven’t come up with it yet. Between the two current unit test fixtures, every combination of the DSL syntax is covered, but I might break it out into some more fixtures to get rid of the #region blocks partitioning the tests.

See you next time…