Porting C# to IronPython — Example 1

Software Engineering 2808 views

Ever since I began reacquainting myself with IronPython, I've been looking for an excuse to use it. However, the opportunity hasn't really presented itself. So, I decided to try porting some existing C# code just as an exercise.

I wanted to start with something simple, and something which could take advantage of Python. Besides, an easy one-to-one port is not very interesting or fun. This lead me to my static WeekHelper utility class. I created the WeekHelper class back in 2005 while working on a grade book web application. Last year, I added a little more functionality to it for a film portal web application.

The Code

Below is the original C# code:

using System;
using System.Collections;
using System.Globalization;

public static class WeekHelper {
    private static Hashtable dayMap;

    public static Hashtable DayMap {
        get {
            if (dayMap == null) {
                dayMap = new Hashtable(7);
                dayMap.Add("Sun", 1);
                dayMap.Add("Mon", 0);
                dayMap.Add("Tue", 6);
                dayMap.Add("Wed", 5);
                dayMap.Add("Thr", 4);
                dayMap.Add("Fri", 3);
                dayMap.Add("Sat", 2);
            }
            return dayMap;
        }
    }

    public static DateTime GetNextMonday(DateTime date) {
        string day = date.ToString("ddd", CultureInfo.CurrentCulture);
        return date.AddDays(Convert.ToInt32(DayMap[day], CultureInfo.CurrentCulture));
    }

    public static DateTime GetWeekMonday(DateTime date) {
        int[] modifier = {
            1, 0, -1, -2, -3, -4, -5
        };

        DayOfWeek day = date.DayOfWeek;

        return date.AddDays(modifier[(int) day]);
    }

    public static DateTime[] GetWeekBeginEnd(DateTime date) {
        DateTime[] result = new DateTime[2];
        result[0] = GetWeekMonday(new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, 0)); // Monday
        DateTime temp = result[0].AddDays(4);
        result[1] = new DateTime(temp.Year, temp.Month, temp.Day, 23, 59, 59, 999); // Friday

        return result;
    }

    public static int GetWeekOfMonth(DateTime date) {
        DateTime workingDate = new DateTime(date.Year, date.Month, 1);
        for (int i = 1; i < 5; i++) {
            DateTime[] weekBeginEnd = GetWeekBeginEnd(workingDate);
            if ((date >= weekBeginEnd[0]) && (date <= weekBeginEnd[1]))
                return i;
            workingDate = weekBeginEnd[1].AddDays(3);
        }
        return 5;
    }

    public static int GetWeekOfYear(DateTime date) {
        CultureInfo ci = CultureInfo.CurrentCulture;
        Calendar calendar = ci.Calendar;
        CalendarWeekRule rule = ci.DateTimeFormat.CalendarWeekRule;

        return calendar.GetWeekOfYear(date, rule, ci.DateTimeFormat.FirstDayOfWeek);
    }
}

And the ported IronPython code:

import clr

clr.AddReferenceByPartialName("System")

from System import DateTime
from System.Globalization import CultureInfo

class WeekHelper:
    dayMap = {"Sun": 1, "Mon": 0, "Tue": 6, "Wed": 5, "Thr": 4, "Fri": 3, "Sat": 2 }

    def GetNextMonday(self, date):
        day = date.ToString("ddd", CultureInfo.CurrentCulture)

        return date.AddDays(self.dayMap[day])

    def GetWeekMonday(date):
        modifier = [1, 0, -1, -2, -3, -4, -5]
        day = int(date.DayOfWeek)

        return date.AddDays(modifier[day])

    def GetWeekBeginEnd(self, date):
        weekBegin = self.GetWeekMonday(DateTime(date.Year, date.Month, date.Day, 0, 0, 0, 0)); # Monday
        temp = weekBegin.AddDays(4)
        weekEnd = DateTime(temp.Year, temp.Month, temp.Day, 23, 59, 59, 999); # Friday

        return weekBegin, weekEnd

    def GetWeekOfMonth(self, date):
        workingDate = DateTime(date.Year, date.Month, 1)
        for i in range(1, 5):
            weekBegin, weekEnd = self.GetWeekBeginEnd(workingDate)
            if date >= weekBegin and date <= weekEnd:
                    return i
            workingDate = weekEnd.AddDays(3)

        return 5

    def GetWeekOfYear(date):
        ci = CultureInfo.CurrentCulture
        calendar = ci.Calendar
        rule = ci.DateTimeFormat.CalendarWeekRule

        return calendar.GetWeekOfYear(date, rule, ci.DateTimeFormat.FirstDayOfWeek)

    GetWeekMonday = staticmethod(GetWeekMonday)
    GetWeekOfYear = staticmethod(GetWeekOfYear)
Testing The Code

Testing the new IronPython code is straightforward. You can write a separate test harness module and run it via the IronPyton Console (ipy.exe). Or, you can create an IronPython ASP.NET Web Site project using Visual Studio 2008 and the ASP.NET Futures release. This is what I chose to do since I had previously used the WeekHelper in two web applications. Besides which, IronPython and ASP.NET are a perfect match.


New Website dialog Solution tree


As you can see from above, IronPython web site projects have an App_Script folder instead of App_Code. Obviously, this is where your script code files should go. Below is the code for the Default.aspx.py file and the generated output:

Python Improvements

Code Aesthetics

I've always liked C#'s terseness, but Python does a better job. Python eliminates the need for line terminators (the semicolon in C#) and enforces code blocks (curly brackets in C#) with white-spaces. This makes your code even more terse and readable.

Import Statements

Visual Studio has a bad habit of including several default import statements whenever you create a new C# class file. Over the years I've developed the habit of documenting which objects I use after the an import statement. Something like this:  using System.Collections; // Hashtable. This way I know to remove the import statement if I ever refactor the code to not need the Hashtable anymore. It was somewhat of a tedious practice but it helped keep my code void of unnecessary references or dependencies. It wasn't until I recently purchased ReSharper 3.0 that I discontinued the practice. ReSharper automatically grays out unused import statements; something Visual Studio should have been doing from the beginning.

In any case, I like the way Python handles imports better. In Python, you can reference the exact objects you need as such:  from System import Convert, DateTime.

Intrinsics

One of the first things you may have noticed about the ported code is that I've ditched .NET's Hashtable in favor of Python's intrinsic dictionary for the dayMap. Python's built-in support for lists, dictionaries, and other sequence data types, in addition to supporting in-line property initializing, makes the code syntax very convenient. Instead of having the lazy loading property populate the Hashtable in C#, I can use a one-liner in Python:  dayMap = {"Sun": 1, "Mon": 0, "Tue": 6, "Wed": 5, "Thr": 4, "Fri": 3, "Sat": 2 }. Nonetheless, this isn't something which is magically exclusive to Python. Microsoft could have given the Hastable in-line initialization if they would have created a corresponding TypeConverter. Also, C# has property initializes in version 3.0. (Finally, no more propagation of the old copy constructor design pattern in C#.)

You may have noticed in the original C# version that I return an open-ended array DateTime[], which references two values, in the GetWeekBeginEnd function. This black-box return value is not something I would normally use. And FxCop will definitely complain about it. However, I did not want to break the functionality into two separate functions, nor did I want to return a less efficient struct (more code to write). Since the method was only used in one place for both projects, I was willing to ignore the puritanical urgings of the SuperEgo for the Id-inspired simple and easy method. While this sort of technique is generally frowned upon in C#, it's perfectly acceptable and encouraged in Python. In Python you can return multiple return values since it supports the concept of a tuple, or a sequence of values. The return statement looks like this:  return weekBegin, weekEnd. Calling a function which returns a tuple looks like this:  weekBegin, weekEnd = self.GetWeekBeginEnd(workingDate).

The for loop is simplified in Python as well. In the

Python Idiosyncrasies

Even though I like Python a lot it does have a few idiosyncrasies you should be aware of when porting from C# or otherwise.

Private Variables

Python does not really support private (or protected, internal etc.) variables. All class members are public. Python does encourage the convention of designation private variables with underscore prefixes

Static Objects
Method Overloading