I won’t go into the details of his talk; you should watch it yourself. It’s well worth the hour-long investment. But his refactoring example reminded me of a refactoring exercise that I recently did.
The code in question is a class that provides date-based services such as calculating X number of business days before or after a given date, and determining whether a specific calendar date is a business date or a holiday. To do so, the service must follow these rules for determining holidays:
- Independence Day, July 4
- Labor Day, first Monday in September
- Thanksgiving Day, fourth Thursday in November
- Day after Thanksgiving Day, fourth Friday in November
- Christmas Day, December 25
- New Year's Day, January 1
- Martin Luther King, Jr. Day, third Monday in January
- President's Day, third Monday in February
- Memorial Day, last Monday in May
Holidays falling on Saturday will be observed on Friday and holidays falling on Sunday will be observed on Monday.We’ve got nine holidays, some determined by a particular position within a month (i.e. “fourth Thursday in November”), and some determined by a particular calendar date (i.e. “July 4th”). The holidays determined by calendar date may need to be adjusted by a day if the date falls on a weekend. The list of holidays is static; it’s not likely to change anytime soon.
The basic interface of the class is:
namespace Your.Namespace.Here
{
public interface IDateService
{
DateTime CalculatePriorBusinessDate(DateTime startDate, int numberOfBusinessDays);
DateTime CalculateSubsequentBusinessDate(DateTime startDate, int numberOfBusinessDays);
bool IsBusinessDate(DateTime calendarDate);
bool IsHolidayDate(DateTime calendarDate);
}
}
1: namespace Your.Namespace.Here
2: {
3: public class DateService : IDateService
4: {
5: private string error = string.Empty;
6:
7: public DateService()
8: {
9: }
10:
11: /// <summary>
12: /// Determines if a calendar date is a valid business date.
13: /// </summary>
14: /// <param name="calendarDate">This is any calendar date</param>
15: /// <returns>returns true, if calendar date is a valid business date, else returns false</returns>
16: ///
17: public bool IsBusinessDate(DateTime calendarDate)
18: {
19: bool isBusinessDate = false;
20: // Weekends and Holidays are not business days
21: if (IsWeekend(calendarDate) || IsHoliday(calendarDate))
22: {
23: isBusinessDate = false;
24: }
25: else
26: isBusinessDate = true;
27: return isBusinessDate;
28: }
29:
30: /// <summary>
31: /// Determines if a calendar date is a valid Holiday.
32: /// </summary>
33: /// <param name="calendarDate">This is any calendar date</param>
34: /// <returns>returns true, if calendar date is a valid holiday, else returns false</returns>
35: ///
36: public bool IsHolidayDate(DateTime calendarDate)
37: {
38: bool isHolidayDate = false;
39:
40: if (IsHoliday(calendarDate))
41: {
42: isHolidayDate = false;
43: }
44: else
45: isHolidayDate = true;
46: return isHolidayDate;
47: }
48:
49: /// <summary>
50: /// Calculates prior business date
51: /// </summary>
52: /// <param name="startDate">This is the starting business date to calculate</param>
53: /// <param name="numberOfBusinessDays">This is the number of prior business days requested;Must be positive integer</param>
54: /// <returns>The calculated business date requested based on number of business days</returns>
55: ///
56: public DateTime CalculatePriorBusinessDate(DateTime startDate, int numberOfBusinessDays)
57: {
58: if (numberOfBusinessDays < 0)
59: {
60: error = "Prior Business Days must be a positive number";
61: throw new Exception(string.Format("reason : {0}", error));
62: }
63: return CalculateBusinessDate(startDate, numberOfBusinessDays * -1);
64: }
65:
66: /// <summary>
67: /// Calculates subsequent business date
68: /// </summary>
69: /// <param name="startDate">This is the starting business date to calculate</param>
70: /// <param name="numberOfBusinessDays">This is the number of subsequent business days requested;Must be positive integer</param>
71: /// <returns>The calculated business date requested based on number of business days</returns>
72: ///
73: public DateTime CalculateSubsequentBusinessDate(DateTime startDate, int numberOfBusinessDays)
74: {
75: if (numberOfBusinessDays < 0)
76: {
77: error = "Subsequent Business Days must be a positive number";
78: throw new Exception(string.Format("reason : {0}", error));
79: }
80: return CalculateBusinessDate(startDate, numberOfBusinessDays);
81: }
82:
83: private DateTime CalculateBusinessDate(DateTime startDate, int numberOfDays)
84: {
85: int busDays = 0;
86: DateTime curDate = startDate;
87: int incr = 1;
88: if (numberOfDays < 0)
89: incr = -1;
90: while (busDays < Math.Abs(numberOfDays))
91: {
92: curDate = curDate.AddDays(incr);
93:
94: // Weekends and Holidays are not business days
95: if (IsWeekend(curDate) || IsHoliday(curDate))
96: { }
97: else
98: busDays = busDays + 1;
99: }
100: return curDate;
101: }
102:
103: private bool IsWeekend(DateTime curDate)
104: {
105: bool isOnWeekend = false;
106:
107: //Check for weekend date
108:
109: if (curDate.DayOfWeek == DayOfWeek.Saturday || curDate.DayOfWeek == DayOfWeek.Sunday)
110: isOnWeekend = true;
111:
112: return isOnWeekend;
113: }
114:
115: private bool IsHoliday(DateTime curDate)
116: {
117: bool returnValue = false;
118:
119: // Check for New Year's, Christmas, and Independence Day
120:
121: if (curDate.Month == 1 && curDate.Day == 1)
122: returnValue = true;
123: if (curDate.Month == 7 && curDate.Day == 4)
124: returnValue = true;
125: if (curDate.Month == 12 && curDate.Day == 25)
126: returnValue = true;
127:
128: // Friday is a holiday if actual holiday falls on Saturday
129:
130: if (curDate.DayOfWeek == DayOfWeek.Friday)
131: {
132: if (curDate.Month == 12 && curDate.Day == 31)
133: returnValue = true;
134: if (curDate.Month == 7 && curDate.Day == 3)
135: returnValue = true;
136: if (curDate.Month == 12 && curDate.Day == 24)
137: returnValue = true;
138: }
139:
140: // Monday is a holiday if actual holiday falls on Sunday
141:
142: if (curDate.DayOfWeek == DayOfWeek.Monday)
143: {
144: if (curDate.Month == 1 && curDate.Day == 2)
145: returnValue = true;
146: if (curDate.Month == 7 && curDate.Day == 5)
147: returnValue = true;
148: if (curDate.Month == 12 && curDate.Day == 26)
149: returnValue = true;
150: }
151:
152: // Test for Monday Holidays
153:
154: if (curDate.DayOfWeek == DayOfWeek.Monday)
155: {
156: if (curDate.Month == 1 && WhichMonday(curDate) == 3)
157: returnValue = true;
158: if (curDate.Month == 2 && WhichMonday(curDate) == 3)
159: returnValue = true;
160: if (curDate.Month == 9 && WhichMonday(curDate) == 1)
161: returnValue = true;
162: if (curDate.Month == 5 && LastMondayInMay(curDate) == true)
163: returnValue = true;
164: }
165:
166: // Test for Thanksgiving
167:
168: if (curDate.DayOfWeek == DayOfWeek.Thursday)
169: {
170: if (curDate.Month == 11 && WhichThursday(curDate) == 4)
171: returnValue = true;
172: }
173: // Test for Friday after Thanksgiving
174:
175: if (curDate.DayOfWeek == DayOfWeek.Friday)
176: {
177: if (curDate.Month == 11 && WhichFriday(curDate) == 4)
178: returnValue = true;
179: }
180: return returnValue;
181: }
182:
183: private int WhichMonday(DateTime curDate)
184: {
185: return WhichDay(curDate, DayOfWeek.Monday);
186: }
187:
188: private int WhichDay(DateTime curDate, DayOfWeek day)
189: {
190: int which = 0;
191: DateTime firstDay = GetFirstDay(curDate, day);
192: while (firstDay <= curDate)
193: {
194: which += 1;
195: if (curDate == firstDay)
196: break;
197: firstDay = firstDay.AddDays(7);
198: if (firstDay.Month != curDate.Month)
199: {
200: throw new Exception("WhichDayError");
201: }
202: }
203: return which;
204: }
205:
206: private DateTime GetFirstMonday(DateTime curDate)
207: {
208: return GetFirstDay(curDate, DayOfWeek.Monday);
209: }
210:
211: private DateTime GetLastMonday(DateTime curDate)
212: {
213: return GetLastDay(curDate, DayOfWeek.Monday);
214: }
215:
216: private DateTime GetFirstDay(DateTime curDate, DayOfWeek day)
217: {
218: DateTime myDate = new DateTime(curDate.Year, curDate.Month, 1);
219: while (myDate.DayOfWeek != day)
220: {
221: myDate = myDate.AddDays(1);
222: }
223: return myDate;
224: }
225:
226: private DateTime GetLastDay(DateTime curDate, DayOfWeek day)
227: {
228: DateTime myDateTemp = curDate.AddMonths(1);
229: DateTime myDate = new DateTime(myDateTemp.Year, myDateTemp.Month, 1);
230: myDate = myDate.AddDays(-1);
231: while (myDate.DayOfWeek != day)
232: {
233: myDate = myDate.AddDays(-1);
234: }
235: return myDate;
236: }
237:
238: private bool LastMondayInMay(DateTime curDate)
239: {
240: bool rv = false;
241: DateTime curMonday = GetLastMonday(curDate);
242: if (curMonday == curDate)
243: rv = true;
244: return rv;
245: }
246:
247: private int WhichThursday(DateTime curDate)
248: {
249: return WhichDay(curDate, DayOfWeek.Thursday);
250: }
251:
252: private DateTime GetFirstThursday(DateTime curDate)
253: {
254: return GetFirstDay(curDate, DayOfWeek.Thursday);
255: }
256:
257: private int WhichFriday(DateTime curDate)
258: {
259: return WhichDay(curDate, DayOfWeek.Friday);
260: }
261:
262: private DateTime GetFirstFriday(DateTime curDate)
263: {
264: DateTime myDate = new DateTime(curDate.Year, curDate.Month, 1);
265: while (myDate.DayOfWeek != DayOfWeek.Friday)
266: {
267: myDate = myDate.AddDays(1);
268: }
269: return myDate;
270: }
271: }
272: }
Some of the code loses readability by being too verbose. Take, for instance, the method used to determine if a particular date is a business date:
public bool IsBusinessDate(DateTime calendarDate)
{
bool isBusinessDate = false;
// Weekends and Holidays are not business days
if (IsWeekend(calendarDate) || IsHoliday(calendarDate))
{
isBusinessDate = false;
}
else
isBusinessDate = true;
return isBusinessDate;
}
public bool IsBusinessDate(DateTime date)
{
return !IsWeekend(date) && !IsHoliday(date);
}
Additionally, read through this block in the middle of the IsHoliday() method:
if (curDate.DayOfWeek == DayOfWeek.Monday)
{
if (curDate.Month == 1 && WhichMonday(curDate) == 3)
returnValue = true;
if (curDate.Month == 2 && WhichMonday(curDate) == 3)
returnValue = true;
if (curDate.Month == 9 && WhichMonday(curDate) == 1)
returnValue = true;
if (curDate.Month == 5 && LastMondayInMay(curDate) == true)
returnValue = true;
}
Taking a step back
Before starting any refactoring, a decided to take a step back and think about how I would go about determining if a date was a holiday. The most straightforward way is to get a list of holidays, and then check my date against that list. That list of holidays must include the nine holidays listed in the rules above, adjusted for weekends if necessary.
I began the refactoring with that process in mind. To start with, I would need to generate the holiday list for the given year. So, I created a method to do so:
public IList<DateTime> HolidaysForYear(int year)
{
var holidayList = new List<DateTime>();
holidayList.Add(NewYearsDay(year));
holidayList.Add(MLKDay(year));
holidayList.Add(PresidentsDay(year));
holidayList.Add(MemorialDay(year));
holidayList.Add(IndependenceDay(year));
holidayList.Add(LaborDay(year));
holidayList.Add(ThanksgivingDay(year));
holidayList.Add(DayAfterThanksgivingDay(year));
holidayList.Add(ChristmasDay(year));
return holidayList;
}
Since some of the holidays may fall on a Saturday or Sunday, they may need to be adjusted back a day (for Saturday) or forward a day (for Sunday). Hence:
private DateTime AdjustForWeekend(DateTime holiday)
{
DateTime adjustedHoliday = holiday;
switch (holiday.DayOfWeek)
{
case DayOfWeek.Saturday:
adjustedHoliday = holiday.AddDays(-1);
break;
case DayOfWeek.Sunday:
adjustedHoliday = holiday.AddDays(1);
break;
default:
break;
}
return adjustedHoliday;
}
private DateTime NewYearsDay(int year)
{
var januaryFirst = new DateTime(year, 1, 1);
return AdjustForWeekend(januaryFirst);
}
Next we have to deal with positional holidays, like the Labor Day (first Monday in September). I kept a couple of the helper methods that the original code had, only slightly altered:
private DateTime GetFirstDay(DateTime date, DayOfWeek dayOfWeek)
{
var dateToCheck = new DateTime(date.Year, date.Month, 1);
while (dateToCheck.DayOfWeek != dayOfWeek)
{
dateToCheck = dateToCheck.AddDays(1);
}
return dateToCheck;
}
private DateTime GetLastDay(DateTime date, DayOfWeek dayOfWeek)
{
var followingMonth = date.AddMonths(1);
var dateToCheck = new DateTime(followingMonth.Year, followingMonth.Month, 1).AddDays(-1);
while (dateToCheck.DayOfWeek != dayOfWeek)
{
dateToCheck = dateToCheck.AddDays(-1);
}
return dateToCheck;
}
private DateTime LaborDay(int year)
{
var firstMondayInSeptember = GetFirstDay(new DateTime(year, 9, 1), DayOfWeek.Monday);
return firstMondayInSeptember;
}
private DateTime ThanksgivingDay(int year)
{
var firstThursdayInNovember = GetFirstDay(new DateTime(year, 11, 1), DayOfWeek.Thursday);
var fourthThursdayInNovember = firstThursdayInNovember.AddDays(21);
return fourthThursdayInNovember;
}
public bool IsHoliday(DateTime date)
{
var dateToCheckFor = new DateTime(date.Year, date.Month, date.Day);
return HolidaysForYear(date.Year).Contains(dateToCheckFor);
}
Here is the final version after refactoring:
1: namespace Your.Namespace.Here
2: {
3: public class DateService : IDateService
4: {
5: public DateProviderService()
6: {
7: }
8:
9: public bool IsBusinessDate(DateTime date)
10: {
11: return !IsWeekend(date) && !IsHoliday(date);
12: }
13:
14: public bool IsWeekend(DateTime date)
15: {
16: var dayOfWeek = date.DayOfWeek;
17: return dayOfWeek == DayOfWeek.Saturday || dayOfWeek == DayOfWeek.Sunday;
18: }
19:
20: public bool IsHoliday(DateTime date)
21: {
22: var dateToCheckFor = new DateTime(date.Year, date.Month, date.Day);
23: return HolidaysForYear(date.Year).Contains(dateToCheckFor);
24: }
25:
26: public DateTime CalculatePriorBusinessDate(DateTime fromDate, int numberOfBusinessDays)
27: {
28: if (numberOfBusinessDays < 0)
29: {
30: string error = "Prior Business Days must be a positive number";
31: throw new Exception(string.Format("reason : {0}", error));
32: }
33: return CalculateBusinessDate(fromDate, numberOfBusinessDays * -1);
34: }
35:
36: public DateTime CalculateSubsequentBusinessDate(DateTime fromDate, int numberOfBusinessDays)
37: {
38: if (numberOfBusinessDays < 0)
39: {
40: string error = "Subsequent Business Days must be a positive number";
41: throw new Exception(string.Format("reason : {0}", error));
42: }
43: return CalculateBusinessDate(fromDate, numberOfBusinessDays);
44: }
45:
46: public IList<DateTime> HolidaysForYear(int year)
47: {
48: var holidayList = new List<DateTime>();
49:
50: holidayList.Add(NewYearsDay(year));
51: holidayList.Add(MLKDay(year));
52: holidayList.Add(PresidentsDay(year));
53: holidayList.Add(MemorialDay(year));
54: holidayList.Add(IndependenceDay(year));
55: holidayList.Add(LaborDay(year));
56: holidayList.Add(ThanksgivingDay(year));
57: holidayList.Add(DayAfterThanksgivingDay(year));
58: holidayList.Add(ChristmasDay(year));
59:
60: return holidayList;
61: }
62:
63: private DateTime CalculateBusinessDate(DateTime fromDate, int numberOfDays)
64: {
65: int businessDayCount = 0;
66: int incrementValue = numberOfDays < 0 ? -1 : 1;
67: DateTime finalDate = fromDate;
68:
69: while (businessDayCount < Math.Abs(numberOfDays))
70: {
71: finalDate = finalDate.AddDays(incrementValue);
72: if (IsBusinessDate(finalDate))
73: {
74: businessDayCount++;
75: }
76: }
77: return finalDate;
78: }
79:
80: private DateTime NewYearsDay(int year)
81: {
82: var januaryFirst = new DateTime(year, 1, 1);
83: return AdjustForWeekend(januaryFirst);
84: }
85:
86: private DateTime MLKDay(int year)
87: {
88: var firstMondayInJanuary = GetFirstDay(new DateTime(year, 1, 1), DayOfWeek.Monday);
89: var thirdMondayInJanuary = firstMondayInJanuary.AddDays(14);
90: return thirdMondayInJanuary;
91: }
92:
93: private DateTime PresidentsDay(int year)
94: {
95: var firstMondayInFebruary = GetFirstDay(new DateTime(year, 2, 1), DayOfWeek.Monday);
96: var thirdMondayInFebruary = firstMondayInFebruary.AddDays(14);
97: return thirdMondayInFebruary;
98: }
99:
100: private DateTime MemorialDay(int year)
101: {
102: var lastMondayInMay = GetLastDay(new DateTime(year, 5, 1), DayOfWeek.Monday);
103: return lastMondayInMay;
104: }
105:
106: private DateTime IndependenceDay(int year)
107: {
108: var julyFourth = new DateTime(year, 7, 4);
109: return AdjustForWeekend(julyFourth);
110: }
111:
112: private DateTime LaborDay(int year)
113: {
114: var firstMondayInSeptember = GetFirstDay(new DateTime(year, 9, 1), DayOfWeek.Monday);
115: return firstMondayInSeptember;
116: }
117:
118: private DateTime ThanksgivingDay(int year)
119: {
120: var firstThursdayInNovember = GetFirstDay(new DateTime(year, 11, 1), DayOfWeek.Thursday);
121: var fourthThursdayInNovember = firstThursdayInNovember.AddDays(21);
122: return fourthThursdayInNovember;
123: }
124:
125: private DateTime DayAfterThanksgivingDay(int year)
126: {
127: return ThanksgivingDay(year).AddDays(1);
128: }
129:
130: private DateTime ChristmasDay(int year)
131: {
132: var decemberTwentyFifth = new DateTime(year, 12, 25);
133: return AdjustForWeekend(decemberTwentyFifth);
134: }
135:
136: private DateTime AdjustForWeekend(DateTime holiday)
137: {
138: DateTime adjustedHoliday = holiday;
139:
140: switch (holiday.DayOfWeek)
141: {
142: case DayOfWeek.Saturday:
143: adjustedHoliday = holiday.AddDays(-1);
144: break;
145:
146: case DayOfWeek.Sunday:
147: adjustedHoliday = holiday.AddDays(1);
148: break;
149:
150: default:
151: break;
152: }
153:
154: return adjustedHoliday;
155: }
156:
157: private DateTime GetFirstDay(DateTime date, DayOfWeek dayOfWeek)
158: {
159: var dateToCheck = new DateTime(date.Year, date.Month, 1);
160: while (dateToCheck.DayOfWeek != dayOfWeek)
161: {
162: dateToCheck = dateToCheck.AddDays(1);
163: }
164: return dateToCheck;
165: }
166:
167: private DateTime GetLastDay(DateTime date, DayOfWeek dayOfWeek)
168: {
169: var followingMonth = date.AddMonths(1);
170: var dateToCheck = new DateTime(followingMonth.Year, followingMonth.Month, 1).AddDays(-1);
171:
172: while (dateToCheck.DayOfWeek != dayOfWeek)
173: {
174: dateToCheck = dateToCheck.AddDays(-1);
175: }
176: return dateToCheck;
177: }
178: }
179: }
No comments:
Post a Comment
Please leave a comment. You can use some HTML tags, such as <b>, <i>, <a>