Get The First Date In A Week, Given The Week’s Number — With Help From BDD

Intro

--

This week I had an interesting task which was to calculate the
first and last day in a week given the week number. In the wider context, this
was part of a filtering system to sift through records and tell the user which
ones were created in a certain week — accessed by its week number.

So, what I’m going to do in this blog post is recreate that
code using SpecFlow to write and implement specs and some basic mathematical formulation
skills (very light, don’t worry).

Before becoming the conjuror of some magical specs, let me
first show you how week numbering works for the hypothetical company this date
calculation is being created for:

Outlook’s Default Week Numbering — Just What We Need

As you can see above, this is exactly how Outlook’s default
week numbers work. Week number 1 contains the last four days of December 2009
and the first three days of January 2010. Obviously the yearly overlap varies
year-by-year.

Writing The Specs

With the formalities out of the way and the proposed
scenario explained, the first step I’ll take is to write the specs. Well, after
creating a new class library called DateCalculationService, and then
adding a new class library to the solution called Specs. Then adding a
reference to TechTalk.SpecFlow.dll and nunit.framework.dll so I can use
SpecFlow.

After adding a feature to the Specs project, I’m just about
ready to get cracking with the specs. But first, let’s just visually confirm
exactly how my solution is currently set up.

Solution Setup After Adding Feature File

The image above shows I’ve named my feature file ReturnsFirstDayInWeek
– trying my best to describe the ‘business rule’ or behavioural requirement of my
date calculation service.

Thinking about the edge cases and possible freak scenarios,
I am certain that the following scenarios should all form part of the tests to
give me confidence that my service will be faultless:

·
First week in a year

·
Second week in a year

·
Last week in a year

·
Random weeks in a year

·
First, second and last week in a leap year

·
Random weeks in different years

And, here are the specs….

Feature:
Should Return The First Day In The Week

In
order to perform date-related filtering

As
a Developer

I
need to be given the first day in a week from a week number and year

Scenario:
First Week In A Year

Given
I have specified the year 2010

And
I have specified week 1

When
I request the first day in the week

Then
I should be given the date 28/12/2009

Scenario:
Second Week In A Year

Given
I have specified the year 2010

And
I have specified week 2

When
I request the first day in the week

Then
I should be given the date 04/01/2010

Scenario:
Last Week In A Year

Given
I have specified the year 2010

And
I have specified week 52

When
I request the first day in the week

Then
I should be given the date 20/12/2010

Scenario:
Randomly-Selected Week In A Year

Given
I have specified the year 2010

And
I have specified week 32

When
I request the first day in the week

Then
I should be given the date 02/08/2010

Scenario:
First Week In A Leap Year

Given
I have specified the year 2008

And
I have specified week 1

When
I request the first day in the week

Then
I should be given the date 31/12/2007

Scenario:
Second Week In A Leap Year

Given
I have specified the year 2008

And
I have specified week 2

When
I request the first day in the week

Then
I should be given the date 07/01/2008

Scenario:
Last Week In A Leap Year

Given
I have specified the year 2008

And
I have specified week 52

When
I request the first day in the week

Then
I should be given the date 22/12/2008

Scenario:
Random Year 2001

Given
I have specified the year 2001

And
I have specified week 13

When
I request the first day in the week

Then
I should be given the date 26/03/2001

Scenario:
Random Year 2005

Given
I have specified the year 2005

And
I have specified week 45

When
I request the first day in the week

Then
I should be given the date 31/10/2005

Implementing Steps

What’s good about SpecFlow, and the way I have written
these specs, is that I can use parameterised steps to save having to write
every single line in each of those scenarios. So essentially, I can just write
the steps for a single scenario — allowing me to add more scenarios which will
use the existing steps, so no new code is needed.

Before writing those steps, I’ll use another good feature of
SpecFlow — I’ll let it tell me exactly what step definitions I need by
attempting to run the tests. The step definitions will then be shown in the
output window.

Ok, I’m back and I’ve implemented my steps, they look
exactly like these:

namespace
Specs

{

[Binding]

public class ReturnsFirstDayInWeek

{

private int
Year { get; set;
}

private int
Week { get; set;
}

private DateTime
FirstDayInWeek { get; set;
}

[Given(@”I
have specified the year (.*)”)]

public void
GivenIHaveSpecifiedTheYear2010(int year)

{

Year = year;

}

[Given(@”I
have specified week (.*)”)]

public void
GivenIHaveSpecifiedWeek52(int week)

{

Week = week;

}

[When(@”I
request the first day in the week”)]

public void
WhenIRequestTheFirstDayInTheWeek()

{

FirstDayInWeek = DateCalculationService.DateCalculationService.GetFirstDayInWeek(Year,
Week);

}

[Then(@”I
should be given the date (.*)”)]

public void
ThenIShouldBeGivenTheDate20122010(string date)

{

var dateValues = date.Split(‘/’).Select(d => Convert.ToInt32(d));

var day = dateValues.ElementAt(0);

var month = dateValues.ElementAt(1);

var year = dateValues.ElementAt(2);

DateTime correctDate = new DateTime(year,
month, day);

Assert.IsTrue(FirstDayInWeek.ToShortDateString()
== correctDate.ToShortDateString(),

“Actual Date Returned: “ +
FirstDayInWeek.ToShortDateString());

}

}

}

I have to point out, first of all that I’ve coded by
intention. The DateCalculationService doesn’t exist and obviously nor does its
GetFirstDayInWeek method. It’s handy to have ReSharper, which makes coding in
this BDD style a lot smoother.

Secondly, look at how little code there is. I just wrote
about 15 specs, and using parameterised steps I can see everything I need on
one screen — without losing any readability. Nice.

Creating A Formula

With my specs all written, and the code to test them in
place, I can now design an algorithm for calculating dates, wrap that in a
method and then test away in a very efficient manner.

So what do I know? I know the variables that could be
involved:

·
Number of weeks in a year

·
Number of days in a week

·
‘Offset’ — number of non-January days in week 1

I’ve got a plan. I’ll imagine that there is no offset — that
the 1st January occurs on Monday (first day in the week). If I then
go to week 2, I know the number of the first day is the 8th –
number of days in the first week plus one. If then take into account the
offset, I would need to push the day number backward in line with the offset.

Ok, here is my formula:

((Week number — 1) * 7) — Offset = Day Number In Year Of
First Day In Week

Using a bit of mathematical convention, we can make that
concise as follows:

x = ((y — 1) * D) — o

So x is the dependent variable, y is the independent (the
one we change), D is the constant days-per-week, and o is the variable offset
for the current year.

Implementing The Code

If my formula is any good, then creating the code should be
pretty easy. As per my intention in my steps, I’m going to make this a static
method on a service class — this could easily be made into an extension method
for the DateTime class. It could also be non-static with different options for
different week-numbering systems. But my friend YAGNI says: “worry about that
when you need to, dear”.

Here we go then; here’s what I have produced:

namespace
DateCalculationService

{

public class DateCalculationService

{

private const int DaysPerWeek = 7;

public static DateTime GetFirstDayInWeek(int year, int week)

{

int yearsOffset = GetYearsOffset(year);

int dayNumberOfFirstDayInWeek = ((week -
1)*DaysPerWeek) — yearsOffset;

DateTime firstDayInYear = new DateTime(year,
01, 01);

DateTime firstDayInWeek =
firstDayInYear.AddDays(dayNumberOfFirstDayInWeek);

return firstDayInWeek;

}

private static int GetYearsOffset(int
year)

{

// create the first day in the year

DateTime firstDayInYear = new DateTime(year,
01, 01);

// find out what day it is and return the offset

switch (firstDayInYear.DayOfWeek)

{

case (DayOfWeek.Monday):

return 0;

case (DayOfWeek.Tuesday):

return 1;

case (DayOfWeek.Wednesday):

return 2;

case (DayOfWeek.Thursday):

return 3;

case (DayOfWeek.Friday):

return 4;

case (DayOfWeek.Saturday):

return 5;

case (DayOfWeek.Sunday):

return 6;

default:

throw new
Exception(“Error
calculating day of week. Input: “ + year);

}

}

}

}

Let’s walk through and I’ll explain what I’ve done.

Starting with the first important line, I’ve actually put
the offset calculation into a separate method. It makes the main method look
cleaner, no doubts, and highlights that the offset is a separate calculation
which could be changed should we have a different week-numbering system.

All the GetYearsOffset method does, is work out, which day
of the week January 1st occurs on for that year. It then knows how
many non-January days are in that first week. Giving me the offset required to
insert into my simple formula.

Following that, I actually calculate the day number (of the
year) for the first day in the supplied week of the supplied year. With this
day number, I just create the first day in the year, and add that number of
days to the first day — giving me the day in the year that represents the day
number I derived from the calculation.

Finally, the day in the week gets returned, all my specs
pass, and the world again is the happy place I love….or is it?

Did the test pass? Does my mathematical brain have ego
issues? Have I made a mess of what I thought to be a simple formula?

Actually, some of my tests did fail, because originally the
wording in my feature file had a few typos. But when I corrected those, I got
this:

Conclusion

Having an organised personality, an organised methodology
(BDD) and a facilitating tool like SpecFlow, means I am really at one with my
current workflow. I start with the challenge of thinking up the scenarios –
which helps me think about what my code will need to do from many angles.

I then get to implement my steps, again with SpecFlow. In
this case, using its parameterised steps, means I have turned a lot of
scenarios, into little amounts of spec code. It saves times, its extensible — I
can add an infinite amount of date checks and I don’t need to write any more
code to test it (you could argue I should, or could use tools like finesse if I’m
so hung up about this feature — but its not quite right for this scenario).

I also have security knowing that if I change my code, my
specs will tell me of any problems. As unit tests would, but just in more
human-readable language. In this case there is no non-technical client, but
there could be.

I also want to mention that having an organised approach
helped me to think about the date calculation itself, without diving in and
hacking around with code to try and get something to work. In this case, it
worked first time. You can’t beat a good bit of planning.

So my findings are: slow down to speed up. Think about what
you’re doing. Have a plan. Make sure you have your next few steps all planned –
or at least know what you need to achieve and the high-level steps you are
going to take to achieve it. Using BDD and tools like SpecFlow will encourage
this thinking.

However, I’m always learning. One day soon I could realise
that I have got things wrong and I’m not as efficient as I could be. All part
of the fun.

--

--

Nick Tune
Nick Tune

Written by Nick Tune

Principal Consultant @ Empathy Software and author of Architecture Modernization (Manning)