Tue Oct 2
{-# LANGUAGE TemplateHaskell, Rank2Types #-}
module A03Sol where
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assert, testCase, (@?=))
import Test.Tasty.TH (defaultMainGenerator)
main = $(defaultMainGenerator)
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.
data Month
= Jan | Feb | Mar | Apr | May | Jun
| Jul | Aug | Sep | Oct | Nov | Dec
deriving (Show, Eq, Ord, Enum, Bounded)
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.)
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 2018
↪ False
– Not divisible by 4isLeapYear 2020
↪ True
– Is divisible by 4isLeapYear 2100
↪ False
– Divisible by 4, but also by 100isLeapYear 2000
↪ True
– Divisible by 4, but also by 400isLeapYear y
| not (y /? 4) = False
| not (y /? 100) = True
| not (y /? 400) = False
| otherwise = True
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.
daysPerMonth y Feb = if isLeapYear y then 29 else 28
daysPerMonth _ Apr = 30
daysPerMonth _ Jun = 30
daysPerMonth _ Sep = 30
daysPerMonth _ Nov = 30
daysPerMonth _ _ = 31
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
tomorrow (Date y m d)
| d < daysPerMonth y m = Date y m (d+1)
| m < Dec = Date y (succ m) 1
| otherwise = Date (succ y) Jan 1
case_dow_show_tue = show Tue @?= "Tue"
case_dow_show_sat = show Sat @?= "Sat"
case_dow_eq = assert $ Wed == Wed
case_dow_neq = assert $ Wed /= Thu
case_dow_ord_1 = assert $ Thu < Fri
case_dow_ord_2 = assert $ Fri < Sun
case_dow_enum_range = length [Mon .. Fri] @?= 5
case_dow_enum_2 = toEnum 2 @?= Wed
case_dow_min = minBound @?= Mon
case_dow_max = maxBound @?= Sun
case_dow_all = length ([minBound..maxBound] :: [DayOfWeek]) @?= 7
case_month_show_mar = show Mar @?= "Mar"
case_month_show_dec = show Dec @?= "Dec"
case_month_eq = assert $ Oct == Oct
case_month_neq = assert $ Jul /= Aug
case_month_ord_1 = assert $ Jul < Aug
case_month_ord_2 = assert $ Jan < Feb
case_month_enum_range = length [Jun .. Aug] @?= 3
case_month_enum_2 = toEnum 2 @?= Mar
case_month_min = minBound @?= Jan
case_month_max = maxBound @?= Dec
case_month_all = length ([minBound..maxBound] :: [Month]) @?= 12
case_date_eq = assert $ Date 1982 Jul 4 == Date 1982 Jul 4
case_date_neq = assert $ Date 1982 Jul 4 /= Date 1982 Jul 5
case_date_lt_day = assert $ Date 1982 Jul 4 < Date 1982 Jul 5
case_date_lt_mon = assert $ Date 1982 Jun 28 < Date 1982 Jul 5
case_date_lt_yr = assert $ Date 1980 Dec 25 < Date 1982 Jul 5
case_leap_2018 = assert $ not (isLeapYear 2018)
case_leap_2020 = assert $ isLeapYear 2020
case_leap_2100 = assert $ not (isLeapYear 2100)
case_leap_2000 = assert $ isLeapYear 2000
case_days_apr = daysPerMonth 2010 Apr @?= 30
case_days_may = daysPerMonth 2010 May @?= 31
case_days_feb00 = daysPerMonth 2000 Feb @?= 29
case_days_feb18 = daysPerMonth 2018 Feb @?= 28
case_days_feb20 = daysPerMonth 2020 Feb @?= 29
case_smart_rejects_neg = smartDate 2013 Jun (-1) @?= Nothing
case_smart_rejects_zero = smartDate 2017 Dec 0 @?= Nothing
case_smart_rejects_dec32 = smartDate 2018 Dec 32 @?= Nothing
case_smart_rejects_feb29 = smartDate 2018 Feb 29 @?= Nothing
case_smart_accepts_feb29 = smartDate 2020 Feb 29 @?= Just (Date 2020 Feb 29)
case_smart_accepts_dec31 = smartDate 2018 Dec 31 @?= Just (Date 2018 Dec 31)
case_smart_rejects_nov31 = smartDate 2018 Nov 31 @?= Nothing
case_tomorrow_day = tomorrow (Date 2018 Mar 9) @?= Date 2018 Mar 10
case_tomorrow_month = tomorrow (Date 2018 Mar 31) @?= Date 2018 Apr 1
case_tomorrow_year = tomorrow (Date 2018 Dec 31) @?= Date 2019 Jan 1
case_tomorrow_leap = tomorrow (Date 2020 Feb 28) @?= Date 2020 Feb 29
case_tomorrow_leap2 = tomorrow (tomorrow (Date 2020 Feb 28)) @?= Date 2020 Mar 1
case_tomorrow_1000 = iterate tomorrow (Date 2018 Sep 26) !! 1000 @?= Date 2021 Jun 22