Home

A day calculator

A day calculator

Koreans celebrate some anniversaries that are measured in days. For instance, one celebrates when a baby is 100 days old. Couples celebrate when they have been together for 100, 200, or even 1000 days. In this lab we will build a calculator for such day calculations.

Your task is to write a Scala script days.scala that can perform three different tasks:

  1. When called with a single argument, it prints the day of the week:
    > scala days.scala 2015/03/20
    2015/03/20 is a Friday
    > scala days.scala 2012/02/10
    2012/02/10 is a Friday
    > scala days.scala 1992/03/21
    1992/03/21 is a Saturday
    
  2. When called with two arguments, it prints the number of days between the two dates:
    > scala days.scala 1995/12/01 2015/03/20
    There are 7049 days between 1995/12/01 and 2015/03/20
    > scala days.scala 2015/03/20 2014/08/24
    There are -208 days between 2015/03/20 and 2014/08/24
    
  3. When called with three arguments, the first one must be a date, the second one either + or -, and the last one an integer. The script then calculates the date at the given distance:
    > scala days.scala 1995/12/01 + 100
    1995/12/01 + 100 days = 1996/03/10
    > scala days.scala 2015/03/20 - 1000
    2015/03/20 - 1000 days = 2012/06/23
    

All dates have to be in the format YYYY/MM/DD. If not, the script should just print "Illegal Date" and do nothing else:

> scala days.scala 1992/8/21 + 100
Illegal date

Your script needs to handle dates between 1901/01/01 and 2099/12/31 only. This simplifies the problem, since leap years are exactly the years divisible by four: 1904, 1908, ..., 1996, 2000, 2004, 2008, ...

Hint

The right way of implementing this task is to create two functions that convert between dates and a day number. Let 1901/01/01 be day number 0, 1901/01/02 day number 1, and so on. Then all computations can be done by taking the day number modulo 7 (to compute the weekday), taking the difference of the day numbers, or adding an integer to the day number and converting back to a date.

Date2num

I suggest you start by implementing a function that converts a date (year, month, day) into a day number. To make your life easy, I've written this function for you:

val monthLength = Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val leapMonthLength = Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val fourYears = (365 + 365 + 365 + 366)

// returns the number of days since 1901/01/01 (day 0)
// returns -1 if illegal, or out of range 1901~2099
def date2num(year: Int, month: Int, day: Int): Int = {
  if (year < 1901 || year > 2099 || month < 1 || month > 12)
    return -1
  val is_leap = (year % 4 == 0)
  val ml = if (is_leap) leapMonthLength else monthLength
  if (day < 1 || day > ml(month-1))
    return -1

  val yearn = year - 1901
  val monthn = month - 1
  val dayn = day - 1

  var total = 0
  total += fourYears * (yearn / 4)
  total += 365 * (yearn % 4)
  for (m <- 0 until monthn)
    total += ml(m)
  total += dayn
  total
}
Copy it (or write your own :-) into a file days.scala and test it in Scala's interactive mode:
> scala -i days.scala 
Loading days.scala...
monthLength: Array[Int] = Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
leapMonthLength: Array[Int] = Array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
fourYears: Int = 1461
date2num: (year: Int, month: Int, day: Int)Int

Welcome to Scala version 2.11.5 (OpenJDK 64-Bit Server VM, Java 1.7.0_75).
Type in expressions to have them evaluated.
Type :help for more information.

scala> date2num(1901, 1, 1)
res1: Int = 0

scala> date2num(1901, 2, 1)
res2: Int = 31

scala> date2num(1901, 3, 1)
res3: Int = 59

scala> date2num(1902, 3, 1)
res4: Int = 424

scala> date2num(1902, 1, 1)
res5: Int = 365

scala> date2num(2012, 2, 13)
res6: Int = 40585

scala> date2num(2015, 3, 20)
res8: Int = 41716

scala> date2num(2012, 2, 30)
res8: Int = -1

scala> date2num(2012, 13, 1)
res9: Int = -1

scala> date2num(2100, 4, 12)
res10: Int = -1
As you can see, when the date is not correct or outside the range 1901..2099, the function returns -1.

String2date

Next, you need a function to convert a command line argument (a string) into a date, that is, a triple of year, month, and day:

// decode a string date
// returns (0,0,0) if not in "YYYY/MM/DD" format
def string2date(s: String): (Int, Int, Int) = {
  // ...
}

Again, test this before you continue using the interactive mode:

scala> string2date("2010/02/12")
res11: (Int, Int, Int) = (2010,2,12)

scala> string2date("1492/02/31")
res12: (Int, Int, Int) = (1492,2,31)

scala> string2date("1492/2/31")
res13: (Int, Int, Int) = (0,0,0)

scala> string2date("14a2/2/31")
res14: (Int, Int, Int) = (0,0,0)

scala> string2date("14a2/02/31")
res15: (Int, Int, Int) = (0,0,0)

The function checks that the string has exactly 10 characters, that characters 4 and 7 are slashes, and that all other characters are digits. Then it converts each piece to an integer using .toInt.

Num2date

Then you need a function that converts a day number back to a date:

def num2date(n: Int): (Int, Int, Int) = { ... }

Again, you should test this function before you continue.

After doing some quick manual tests, at this point it makes sense to write a separate testing function that checks that num2date and date2num implement the inverse function. Here is such a test function:

def testAll() {
  for (year <- 1901 to 2099) {
    for (month <- 1 to 12) {
      for (day <- 1 to 28) {
        val n = date2num(year, month, day)
	val (y,m,d) = num2date(n)
	if (year != y || month != m || day != d)
	  println(n, y, m, d)
      }
    }
  }
}

Put it together

Now all you need to do is to check the number of command line arguments and handle each case.

Bonus problems

When you are done and you still have time left in the lab, do the following:

First, when there is only one argument, print a calendar of the month containing the given date:

> scala days.scala 2012/02/13
2012/02/13 is a Monday

   February 2012      
Su Mo Tu We Th Fr Sa  
          1  2  3  4  
 5  6  7  8  9 10 11  
12 13 14 15 16 17 18  
19 20 21 22 23 24 25  
26 27 28 29           

Second, modify your script so that it works correctly for all dates starting with 1600/01/01. Remember that a year is a leap year if it is divisible by 400, or if it is divisible by 4 but not by 100. So 1600, 2000, 2400 are leap years, but 1700, 1800, 1900, 2100, 2200 are not.