Tuesday, July 31, 2012

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.

Thursday, July 26, 2012

HTML5 & Offline Rocks

Even though I know that the HTML 5 standards and features are slowly becoming old news, there are still a number of features that we haven't incorporated into a lot of projects until now.

So after doing some research on the topic of Offline applications and how to use the local storage I set out to build a simple proof of concept.


Using the information I found on the following sites :

http://www.code-magazine.com/Article.aspx?quickid=1112051

http://www.w3.org/TR/offline-webapps/#offline

http://html5demos.com/html5demo.appcache

http://www.html5rocks.com/en/tutorials/webdatabase/todo/

I was quickly able to create a simple project that allows for users to register themselves.
Registration is then handled by a manager class that does nothing more than append to a file.

If you are in offline mode however, the system still allows for the pages to load and to register. Once the internet connection is recovered, the page will synchronize the registrations to the manager, in result adding the registrations to the flat-file.

Some code snippets below :

//Sync to server


function syncToServer() {
            for (var i = 0; i < localStorage.length; i++) {
                var model = getModel(i);
                if (model.IsDirty) {
                    $.post("/home/RegisterSubscription", model,
                            function (data) {
                                var key = data.Key;
                                var m = getModel(key);
                                m.IsDirty = false;
                                localStorage.removeItem(key);
                                logMessage("'" + m.Name + "' saved to server");
                            });
                }
            }
            logMessage("Sync to server complete");
        }

//Save locally


function saveToLocal() {
            var model = getModel(subscriberIndex);
            model.IsDirty = true;
            model.Name = $("#Name").val();
            localStorage.setItem(subscriberIndex, JSON.stringify(model));
            logMessage("'" + model.Name + "' saved locally.");
            subscriberIndex++;
            $("#Name").val = "";
        }

//Check statusses


function reportOnlineStatus() {
            var status = $("#onlineStatus");

            if (isOnLine()) {
                status.text("Online");
                status.removeClass("offline").addClass("online");
            }
            else {
                status.text("Offline");
                status.removeClass("online").addClass("offline");
            }
        }

//React to cache and state events


window.applicationCache.onupdateready = function (e) {
            applicationCache.swapCache();
            window.location.reload();
        }

        window.addEventListener("online", function (e) {
            reportOnlineStatus();
            syncToServer();
        }, true);

        window.addEventListener("offline", function (e) {
            reportOnlineStatus();
        }, true);



Creating a viable Poc took me approx 2 - 3 hours in asp.net MVC with Razor in Visual Studio.
Feel free to contact me to get the entire code base of this project.
All in all fun to see how easy this is.

Wednesday, July 25, 2012

Offline HTML 5 pages

Since I've been wanting to look into creating an application that is capable to work in offline modus asswell i've decided to create a blog so I can report on my progress and other nice to know implementation tricks or technologies.