Friday, February 11, 2011

Manipulating time

Handling time using the standard Java libraries can be a real pain. I am writing a webapp that uses pretty complicated date arithmetic, and while Java's Calendar object works, clients of the library are forced to work with a mutable object, thus being rather verbose to accomplish a simple task.

Enter ScalaCalendar to save the day! I began writing a Scala wrapper for the Java Calendar object, and I'm mostly happy how it turned out. Here's an example of how it works:

// Importing the base ScalaCalendar object like so
// allows you to do some pretty powerful things.
import com.philipcali.calendar.ScalaCalendar._

// Let's work with the simple things
val rightNow = java.util.Date() // or createTime() or "now"
val tomorrow = rightNow + 1 day
val nextMonth = rightNow + 1 month
val yesterday = rightNow - 1 day   

// Generate a duration is rather simple too
val span = rightNow to (rightNow + 1 week)

rightNow isIn span // returns true

rightNow isWeekend
rightNow isWeekday

span.delta.days // returns 7
span.delta.months // returns 0

// Calendar fields
// Your standard field have a name method
// which tries to define the field in English
rightNow.time == rightNow.millisecond
rightNow.second.value
rightNow.minute.value
rightNow.hour.value
rightNow.day.value
rightNow.day.inWeek
rightNow.day.inYear
rightNow.week.value
rightNow.week.InYear
rightNow.month.value
rightNow.year.value

// Work with strings
val allFeb = "2/1/2011" to ("3/1/2011" - 1 day)

// This is where it gets good:
// A duration is traversable by any other duration
span.by(1 day) foreach { dayDuration
  println(dayDuration.day.name)
}

// Only work with MWF days
val mwf = span.by(1 day) filter(_.day.name match {
  case "Monday" | "Wednesday" | "Friday" => true
  case _ => false
})

// Only weekends
span.by(1 day) filter(_.isWeekend) foreach { time =>
  println("Partying it up on %s!" format(time.day.name))
}

// Using this logic, building a calendar table is painless
val calHtml = 
<table> {
  "2/1/2011".calendarMonth.traverse(1 week) { weekD =>
    <tr> {
      weekD.traverse(1 day) { dayD =>
        <td>{ dayD.day.value }</td>
      }
    }</tr>
  }
}</table>

// There's convenience methods for generating a Calendar month
// week, and day
"2/1/2011".calendarMonth // contains 1/30/2011 - 3/5/2011
"2/1/2011".calendarWeek // contains 1/30/2011 - 2/5/2011
"2/1/2011".calendarDay // contains 00:00:00 - 23:59:59

// You can even traverse a timespan in reverse
val countdown = ("2/28/2011" to "2/1/2011").by(1 day) map(_.day.value)
countdown.take(5).mkString(",") // "28,27,26,25,24"

// Traverse a duration at your own space
val february = "2/1/2011" to "2/1/2011" + (1 month)

while(february.hasNext) {
  february.next(1 day)
  // do something
}

// Construct something wacky
// This gets the weekdays every other week in the month
for(days <- february.by(1 week); if(days.week.value % 2 == 0)
     day <- weeks.by(1 day); if day.isWeekDay) {
  println(day.day)
}

The by method splits the durations into a lists of durations, while the traverse method splits and applies a function you give to transform that duration into something meaningful. The ScalaCalendar integrates with java.util.Date and java.util.Calendar seamlessly.

Most people will probably look at this and face palm, but it satisfied my needs for this particular project quite nicely.

I'm going to release the calendar api on my github account very soon.

-- Philip Cali

3 comments:

  1. Wow, very nice Phil! Impressive. You came up with an absolutely great API!

    rightNow + 1 day // fantastic stuff here!!

    Two quick questions:

    1) Why does February 1st's calendarMonth contain January 30th and March 5th?

    2) Have you considered calling it Scalendar? :-)

    ReplyDelete
  2. Ha, thanks Brad!

    1) It was important for my Implementation that the "Calendar month" was a month representation like you would see on Google calendar, yet I suppose I could pass some argument in there to make it more flexible. (Whether or not to contain days from other months in there.)

    2) No I haven't, but that sounds like a winner to me!

    ReplyDelete