Assignment 3 solutions

Tue Oct 2

Calendar types

Define type synonyms Year and Day, both of which will be equivalent to the type Int.

Next, define a data type DayOfWeek. The constructors should all be three-letter abbreviations of the days of the week in English, such as Mon, Fri.

Make the DayOfWeek type derive the type classes Show, Eq, Ord, Enum, and Bounded.

For the Ord, Bounded, and Enum instances to be consistent with the test cases, make the first day of the week be Mon and the last day be Sun.

Next, define a Month data type. The constructors should all be three-letter abbreviations of the month names in English, such as Sep and Jul. It should also derive Show, Eq, Ord, Enum, and Bounded so that the months appear in calendar order.

The last type that we’ll need is Date, which should take three arguments, of types Year, Month, and Day (in that order) with one constructor also named Date. It should derive Eq, Ord, and Show.

(The derived Ord should work well to determine chronological ordering of dates because the Year/Month/Day format is lexicographic.)

Smart date constructor

One potential issue with the Date constructor we just defined is that it accepts any integers for the year and day:

ghci> Date 2018 Mar 32
Date 2018 Mar 32
ghci> Date maxBound Apr minBound
Date 9223372036854775807 Apr (-9223372036854775808)

It’s common in Haskell to define a function we can use in place of the constructor, that will do proper error-checking and only return valid values of the constructed type.

So in this section we’ll define a smart constructor having this type:

If the date is not valid, it should return Nothing. If it is valid, it returns the Date value wrapped with Just:

ghci> smartDate 2018 Mar 32
Nothing
ghci> smartDate maxBound Apr minBound
Nothing
ghci> smartDate 2018 Mar 31
Just (Date 2018 Mar 31)

I recommend decomposing this problem by defining the following two helper functions:

The rule for leap years may be more complicated than you remember. Years divisible by 4 are leap years, unless they are also divisible by 100, but not by 400. Got that?

  • isLeapYear 2018False – Not divisible by 4
  • isLeapYear 2020True – Is divisible by 4
  • isLeapYear 2100False – Divisible by 4, but also by 100
  • isLeapYear 2000True – Divisible by 4, but also by 400

This function will basically implement that silly poem (that I can never remember): “30 days has November, April, June, and September. All the rest have 31 except February something something.” The function needs the year as an argument so it can give February 29 days in leap years, and 28 days otherwise.

Better luck tomorrow

Finally, define a function tomorrow that will take a valid Date and return the next valid Date.

It should flip over seamlessly from one month to the next and one year to the next:

ghci> tomorrow (Date 2018 Mar 9)
Date 2018 Mar 10
ghci> tomorrow (Date 2018 Mar 31)
Date 2018 Apr 1
ghci> tomorrow (Date 2018 Dec 31)
Date 2019 Jan 1
ghci> tomorrow (Date 2020 Feb 28)
Date 2020 Feb 29
ghci> tomorrow (tomorrow (Date 2020 Feb 28))
Date 2020 Mar 1

Here is a clever usage to enumerate many days into the future using the iterate function. (iterate returns an infinite list, so you must use take for this to terminate.)

ghci> mapM_ print $ take 10 $ iterate tomorrow (Date 2018 Sep 26)
Date 2018 Sep 26
Date 2018 Sep 27
Date 2018 Sep 28
Date 2018 Sep 29
Date 2018 Sep 30
Date 2018 Oct 1
Date 2018 Oct 2
Date 2018 Oct 3
Date 2018 Oct 4
Date 2018 Oct 5

Tests

DayOfWeek

Month

Date

isLeapYear

daysPerMonth

smartDate

tomorrow