The Mastermind gameCS109 Programming ProjectsThe area of the circleA 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 script days.kts that can perform three different tasks:

  1. When called with a single argument, it prints the day of the week:
    $ kts days.kts 2017/03/20
    2017/03/20 is a Monday
    $ kts days.kts 2012/02/10
    2012/02/10 is a Friday
    $ kts days.kts 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:
    $ kts days.kts 1992/03/21 2017/03/20
    There are 9130 days between 1992/03/21 and 2017/03/20
    $ kts days.kts 2017/03/20 2015/12/24
    There are -452 days between 2017/03/20 and 2015/12/24
    
  3. When called with three arguments, the first one must be a date, the second one either plus or minus, and the last one an integer. The script then calculates the date at the given distance:
    $ kts days.kts 2017/03/20 minus 100
    2017/03/20 minus 100 days = 2016/12/10
    $ kts days.kts 2017/03/20 plus 100
    2017/03/20 plus 100 days = 2017/06/28
    

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

$ kts days.kts 1992/8/21 plus 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 = arrayOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val leapMonthLength = arrayOf(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
fun 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 in 0 until monthn)
    total += ml[m]
  total += dayn
  return total
}
Copy it (or write your own :-) into a file days.kts and test it in the interactive mode:
$ ktc
Welcome to Kotlin version 1.0.4 (JRE 1.8.0_91)
Type :help for help, :quit for quit
>>> :load days.kts
>>> date2num(1901, 1, 1)
0
>>> date2num(1901, 2, 1)
31
>>> date2num(1901, 3, 1)
59
>>> date2num(1902, 3, 1)
424
>>> date2num(1902, 1, 1)
365
>>> date2num(2012, 2, 13)
40585
>>> date2num(2015, 3, 20)
41716
>>> date2num(2012, 2, 30)
-1
>>> date2num(2012, 13, 1)
-1
>>> date2num(2100, 4, 12)
-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
fun string2date(s: String): Triple<Int, Int, Int> {
  // ...
}

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().

Test your function before you continue. Use the interactive mode for testing:

>>> string2date("2010/02/12")
(2010, 2, 12)
>>> string2date("1492/02/31")
(1492, 2, 31)
>>> string2date("1492/2/31")
(0, 0, 0)
>>> string2date("14a2/2/31")
(0, 0, 0)
>>> string2date("14a2/02/31")
(0, 0, 0)

Num2date

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

fun num2date(n: Int): Triple<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:

fun testAll() {
  for (year in 1901 .. 2099) {
    for (month in 1 .. 12) {
      for (day in 1 .. 28) {
        val n = date2num(year, month, day)
	val (y,m,d) = num2date(n)
	if (year != y || month != m || day != d)
	  println("Incorrect conversion day number $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, improve your script as follows:

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

$ kts days.kts 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           
The Mastermind gameCS109 Programming ProjectsThe area of the circleA day calculator