Date Ranges in C#

DateRange utility class

Throughout development there are a number of typical things that tend to recur. One of these is implementing a means to display start and end dates for News, Events, Symposiums, Lives etc etc

Writing a little class that manages printing these dates seemed like a necessary task that just had to be done in order to make this tedious task be a thing of the past.

Therefore I created my "DateRange" class (what's in a name) that should handle this entirely for you.

The Idea


The intriquities of displaying a datetime range should be an abstraction. You should only need to worry about passing the correct dates (start and end - if applicable) to the Utility class combined with an optional formatting. The class can be entirely configured through use of Enumerations for all the separate settings such as DayFormat etc.
An additional class was used to implement this utility, the StringValue attribute class. You can easily use thie class to add easier functionality to enums or to add cleaner values. But in this context it is only used as a utility class.

The implementation and examples :

The defaults for the implementation of the DateRange are :

dayFormat = DayFormat.LeadingZero;
monthFormat = MonthFormat.LeadingZero;
yearFormat = YearFormat.Full;
segmentOrder = SegmentOrder.DMY;


//fromDate = 5 may 2014
//toDate = 8 may 2014

DateRange range = new DateRange(fromDate, toDate, CultureInfo);
range.monthFormat = DateRange.MonthFormat.Full;
range.ToString(); //Yields 05-08 May 2014

range.segmentOrder = DateRange.SegmentOrder.YMD;
range.showDaysIndividually = true;
range.dayFormat = DateRange.DayFormat.NoLeadingZero;
range.ToString(); //Yields 5-6-7-8 May 2014

//fromDate = 28 may 2014
//toDate = 3 june 2014
range.segmentOrder = DateRange.SegmentOrder.MDY;
range.monthFormat = DateRange.MonthFormat.Full;
range.ToString(); //Yields May 28 - June 3 2014

range.segmentOrder = DateRange.SegmentOrder.MDY;
range.monthFormat = DateRange.MonthFormat.Full;
range.mergeYears = false;  
range.ToString(); //Yields May 28 2014 - June 3 2014  


range.segmentOrder = DateRange.SegmentOrder.MDY;
range.showYears= false;
range.ToString(); //Yields May 28 - June 3

These are only a number of examples on how to use the range class. Not providing an end date to the range is not a problem either. You can also always hide any element, change it's formatting, location, bundling (display the same value twice or not at all, such as the month or year), change the seperator etc.

The DateRange Class :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI.WebControls;
using System.Globalization;

namespace Project.Helper
{
    public class DateRange
    {
        public enum DayFormat
        {
            [StringValue("dddd")] Full,
            [StringValue("ddd")] Abbreviation,
            [StringValue("dd")] LeadingZero,
            [StringValue("d")] NoLeadingZero
        }

        public enum MonthFormat
        {
            [StringValue("MMMM")] Full,
            [StringValue("MMM")] Abbreviation,
            [StringValue("MM")] LeadingZero,
            [StringValue("M")] NoLeadingZero
        }

        public enum YearFormat
        {
            [StringValue("yyyy")] Full,
            [StringValue("yy")] LeadingZero
        }

        public enum SegmentOrder
        {
            [StringValue("dmy")] DMY,
            [StringValue("dym")] DYM,
            [StringValue("myd")] MYD,
            [StringValue("mdy")] MDY,
            [StringValue("ymd")] YMD,
            [StringValue("ydm")] YDM
        }

        public DateTime From { get; set; }
        public DateTime To { get; set; }
        private CultureInfo culture = new System.Globalization.CultureInfo("en-gb");

        public DateRange(DateTime from, DateTime to, CultureInfo cultureInfo = null)
        {
            From = from;
            if (from < to)
            {
                To = to;
            }
            if (cultureInfo != null)
            {
                culture = cultureInfo;
            }
        }

        public DayFormat dayFormat = DayFormat.LeadingZero;
        public MonthFormat monthFormat = MonthFormat.LeadingZero;
        public YearFormat yearFormat = YearFormat.Full;
        public SegmentOrder segmentOrder = SegmentOrder.DMY;
        public char seperator = '-';
        public bool showDays = true;
        public bool showMonths = true;
        public bool showYears = true;
        public bool mergeDays = true;
        public bool mergeYears = true;
        public bool showDaysIndividually = false;
        public bool bundleDayAndMonth = false;

        private bool monthsBuilt = false;
        private bool daysBuilt = false;
        private bool yearsBuilt = false;

        public override string ToString()
        {
            DetermineDayAndMonthBundling();
            char[] segments = StringValueAttribute.GetStringValue(segmentOrder).ToCharArray();
            string dateRange = "";
            foreach (char segment in segments)
            {
                switch (segment)
                {
                    case 'd': dateRange += " " + BuildDays(); break;
                    case 'm': dateRange += " " + BuildMonths(); break;
                    case 'y': dateRange += " " + BuildYears(); break;
                    default: break;
                }
            }
            return dateRange.Trim();
        }

        private void DetermineDayAndMonthBundling()
        {
            bundleDayAndMonth |= (From.Month != To.Month);
        }

        private string BuildMonths()
        {
            string months = "";
            if (!(monthsBuilt || bundleDayAndMonth) && showMonths)
            {
                string format = StringValueAttribute.GetStringValue(monthFormat);
                months = From.ToString(format, culture);
            }
            monthsBuilt = true;
            return months;
        }

        private string BuildYears()
        {
            string years = "";
            if (!yearsBuilt && showYears)
            {
                string format = StringValueAttribute.GetStringValue(yearFormat);
                years = From.ToString(format);
                if (To != DateTime.MinValue)
                {
                    string toYear = To.ToString(format);
                    if (!(mergeYears && years == toYear))
                    {
                        years += seperator + toYear;
                    }
                }
            }
            yearsBuilt = true;
            return years;
        }

        private string BuildDays()
        {
            string days = "";
            if (!daysBuilt && showDays)
            {               
                days = HandleDay(From);
                if (To != DateTime.MinValue)
                {
                    if (showDaysIndividually)
                    {
                        double daysBetween = (To - From).TotalDays;
                        DateTime workingDate = From;
                        for (int i = 0; i < daysBetween; i++)
                        {
                            workingDate = workingDate.AddDays(1);
                            days += seperator + 
                            workingDate.ToString(StringValueAttribute.GetStringValue(dayFormat));
                        }
                    }
                    else
                    {
                        string toDay = HandleDay(To);
                        if (!(mergeDays && days == toDay))
                        {
                            days += seperator + toDay;
                        }
                    }
                }
            }
            daysBuilt = true;
            return days;
        }

        private string HandleDay(DateTime date)
        {
            string dayForm = StringValueAttribute.GetStringValue(dayFormat);
            string monthForm = StringValueAttribute.GetStringValue(monthFormat);
            string day = date.ToString(dayForm);
            if (bundleDayAndMonth)
            {
                string month = date.ToString(monthForm, culture);
                day = monthsBuilt ? month + " " + day : day + " " + month;
            }
            return day;
        }
    }
}

The StringValue class:

using System;
using System.Collections;
using System.Reflection;

namespace Project.Helper
{
    public class StringValueAttribute : Attribute
    {
        private static readonly Hashtable _stringValues = new Hashtable();
        private readonly string _value;

        // object for locking in methods
        private static object syncRootMethods = new object();

        public StringValueAttribute(string value)
        {
            _value = value;
        }

        public string Value
        {
            get { return _value; }
        }

        public static string GetStringValue(Enum value)
        {
            string output = null;
            if (value != null)
            {
                Type type = value.GetType();

                lock (syncRootMethods)
                {
                    //Check first in our cached results...
                    if (_stringValues.ContainsKey(value))
                    {
                        output = ((StringValueAttribute)_stringValues[value]).Value;
                    }
                    else
                    {
                        //Look for our 'StringValueAttribute' in the field's custom attributes
                        FieldInfo fi = type.GetField(value.ToString());
                        StringValueAttribute[] attrs = fi.GetCustomAttributes(typeof(StringValueAttribute), false) as StringValueAttribute[];
                        if (attrs != null && attrs.Length > 0)
                        {
                            if (!_stringValues.ContainsKey(value))
                            {
                                _stringValues.Add(value, attrs[0]);
                            }
                            output = attrs[0].Value;
                        }
                    }
                }
            }
            return output;
        }

        public static object GetEnum(Type enumType, string stringValue)
        {
            Array enumValues = Enum.GetValues(enumType);
            foreach (object enumValue in enumValues)
            {
                FieldInfo fi = enumType.GetField(enumValue.ToString());
                StringValueAttribute[] attrs = fi.GetCustomAttributes(typeof(StringValueAttribute), false) as StringValueAttribute[];
                if (attrs != null && attrs.Length > 0 && attrs[0].Value == stringValue)
                {
                    return enumValue;
                }
            }

            throw new ArgumentException(string.Format("No string value {0} for enum {1}", stringValue, enumType.Name));
        }
    }
}

Conclusion

I hope this utility class might help you on your way on the repetitive task of handling dates and time ranges. It has helped me on numerable implementations and get's tweaked or updated from time to time. Just try to keep it clean and lean since that is it's intended purpose.
And no, it doesn't handle hours/minutes/seconds etc yet. Perhaps it should be upgraded to do that, something I will do when I have a bridge that requires crossing.

Comments

  1. Its a good start. A public bool Contains(DateTime targetDate) method would be useful. DayCount, WorkdayCount, WeekCount, YearCount, SecondCount properties or methods would also be of use.

    ReplyDelete

Post a Comment

Popular posts from this blog

Stupid exception: Device is not ready – System.IOException .net

Sitecore 8.2 in-depth preview + 8.3 update info and Sitecore Ecommerce information