Skip to content

Instantly share code, notes, and snippets.

@jonas
Last active June 8, 2016 14:11
Show Gist options
  • Save jonas/08605295e7a12fbffc469001293278e0 to your computer and use it in GitHub Desktop.
Save jonas/08605295e7a12fbffc469001293278e0 to your computer and use it in GitHub Desktop.
WIP porting Luigi's DateInterval to Scala
import org.joda.time.DateTime
import org.joda.time.DateTimeFieldType
import org.joda.time.format.{ DateTimeFormat, DateTimeFormatter }
import scala.util.Try
object utcDateTime {
val UTC = org.joda.time.DateTimeZone.UTC
def apply(year: Int, month: Int = 1, day: Int = 1, hour: Int = 0): DateTime =
new DateTime(year, month, day, hour, 0, UTC)
}
case class DateIntervalFormatter[T](
pattern: String,
convert: DateTime => T) {
val format = DateTimeFormat.forPattern(pattern).withZone(UTC)
def parse(input: String): Option[T] =
Try(format.parseDateTime(input)).map(convert).toOption
}
/**
* Base class for date intervals.
*
* The interval is representated as the half open range between `from` and `to`.
* For instance, May 2014 is represented as `from = 2014-05-01`, `to = 2014-06-01`.
*
* Example:
* ```
* scala> println(Month(2016, 6).prev)
* 2016-05
* ```
*/
abstract class DateInterval[T <: DateInterval[T]: DateIntervalFormatter] extends Ordered[T] with Traversable[DateTime] {
def from: DateTime
def to: DateTime
override def toString = from.toString(implicitly[DateIntervalFormatter[T]].format)
override def foreach[U](f: DateTime => U) = dates.foreach(f)
override def compare(that: T) = from.compareTo(that.from)
/** Returns a list of dates in this date interval. */
def dates: Seq[DateTime] = {
val dates = scala.collection.mutable.ArrayBuffer.empty[DateTime]
var d = from
while (d.isBefore(to)) {
dates.append(d)
d = d.plusDays(1)
}
dates
}
/** Same as `dates` but returns 24 times more info: one for each hour. */
def hours: Seq[DateTime] = {
for (date <- dates; hour <- 0 to 23)
yield date.withHourOfDay(hour)
}
/** Returns the preceding corresponding date interval (eg. May -> April). */
def prev: T = {
implicitly[DateIntervalFormatter[T]].convert(from.minusDays(1))
}
/** Returns the subsequent corresponding date interval (eg. 2014 -> 2015). */
def next: T = {
implicitly[DateIntervalFormatter[T]].convert(to)
}
}
object Date {
implicit val converter = DateIntervalFormatter[Date](
"yyyy-MM-dd",
date => Date(
date.get(DateTimeFieldType.year),
date.get(DateTimeFieldType.monthOfYear),
date.get(DateTimeFieldType.dayOfMonth)
)
)
val parse = converter.parse _
}
/** Interval for one day. */
case class Date(year: Int, month: Int, day: Int) extends DateInterval[Date] {
val from = utcDateTime(year, month, day)
val to = from.plusDays(1)
}
object Week {
implicit val converter = DateIntervalFormatter[Week](
"yyyy-'W'ww",
date => Week(
date.get(DateTimeFieldType.year),
date.get(DateTimeFieldType.weekOfWeekyear)
)
)
val parse = converter.parse _
}
/**
* ISO 8601 week. Note that it has some counterintuitive behavior around new year.
* For instance Monday 29 December 2008 is week 2009-W01, and Sunday 3 January 2010 is week 2009-W53
* This example was taken from from http://en.wikipedia.org/wiki/ISO_8601#Week_dates
*/
case class Week(year: Int, week: Int) extends DateInterval[Week] {
val from = utcDateTime(year).plusWeeks(week)
val to = from.plusWeeks(1)
}
object Month {
implicit val converter = DateIntervalFormatter[Month](
"yyyy-MM",
date => Month(
date.get(DateTimeFieldType.year),
date.get(DateTimeFieldType.monthOfYear)
)
)
val parse = converter.parse _
}
case class Month(year: Int, month: Int) extends DateInterval[Month] {
val from = utcDateTime(year, month)
val to = from.plusMonths(1)
}
object Year {
implicit val converter = DateIntervalFormatter[Year](
"yyyy",
date => Year(date.get(DateTimeFieldType.year))
)
val parse = converter.parse _
}
case class Year(year: Int) extends DateInterval[Year] {
val from = utcDateTime(year)
val to = from.plusYears(1)
}
object CustomDateInterval {
implicit val converter = DateIntervalFormatter[CustomDateInterval](
"yyyy-MM-dd",
date => throw new NotImplementedError
)
def parse(input: String): Option[CustomDateInterval] =
Try {
val Array(from, to) = input.split(":").map(converter.format.parseDateTime)
CustomDateInterval(from, to)
}.toOption
val parse = converter.parse _
}
/** Representation for arbitrary date intervals. */
case class CustomDateInterval(val from: DateTime, val to: DateTime) extends DateInterval[CustomDateInterval] {
override def toString = {
import CustomDateInterval.converter.format
s"${from.toString(format)}:${to.toString(format)}"
}
}
import java.io.File
import scala.concurrent.Future
import org.scalatest._
import org.scalatest.concurrent.ScalaFutures
class DateIntervalSpec extends FreeSpec {
"Date" - {
"should print date" in {
assert(Date(1999, 12, 31).toString == "1999-12-31")
assert(Date(2016, 6, 5).toString == "2016-06-05")
}
"should parse date" in {
val dt = Date.parse("2016-06-05")
assert(dt == Some(Date(2016, 6, 5)))
}
"should have next/prev" in {
val dt = Date(2016, 6, 5)
assert(dt.prev < dt)
assert(dt < dt.next)
assert(dt.prev == Date(2016, 6, 4))
assert(dt.next == Date(2016, 6, 6))
val dt2 = Date(1999, 12, 31)
assert(dt2.prev == Date(1999, 12, 30))
assert(dt2.next == Date(2000, 1, 1))
}
"should have iterator for dates" in {
val dt = Date(2016, 6, 5)
assert(dt.dates.size == 1)
val iter = for (v <- dt.dates) yield v
assert(iter.size == 1)
}
"should have iterator for hours" in {
val dt = Date(2016, 6, 5)
assert(dt.hours.size == 24)
val iter = for (v <- dt.hours) yield v
assert(iter.size == 24)
}
}
"Week" - {
"should print date" in {
assert(Week(1999, 52).toString == "1999-W52")
assert(Week(2016, 1).toString == "2016-W01")
}
"should parse date" in {
assert(Week.parse("2016-W01") == Some(Week(2016, 1)))
assert(Week.parse("1999-W51") == Some(Week(1999, 51)))
}
"should have next/prev" in {
val dt = Week(2016, 6)
println(dt.prev)
println(dt)
assert(dt.prev < dt)
assert(dt < dt.next)
assert(dt.prev == Week(2016, 5))
assert(dt.next == Week(2016, 7))
val dt2 = Week(1999, 52)
assert(dt2.prev == Week(1999, 51))
assert(dt2.next == Week(2000, 1))
}
"should have iterator for dates" in {
val dt = Week(2016, 6)
assert(dt.dates.size == 7)
val iter = for (v <- dt.dates) yield v
assert(iter.size == 7)
}
"should have iterator for hours" in {
val dt = Week(2016, 6)
assert(dt.hours.size == 7 * 24)
val iter = for (v <- dt.hours) yield v
assert(iter.size == 7 * 24)
}
}
"Month" - {
"should print date" in {
assert(Month(1999, 12).toString == "1999-12")
assert(Month(2016, 6).toString == "2016-06")
}
"should parse date" in {
val dt = Month.parse("2016-06")
assert(dt == Some(Month(2016, 6)))
}
"should have next/prev" in {
val dt = Month(2016, 6)
assert(dt.prev < dt)
assert(dt < dt.next)
assert(dt.prev == Month(2016, 5))
assert(dt.next == Month(2016, 7))
val dt2 = Month(1999, 12)
assert(dt2.prev == Month(1999, 11))
assert(dt2.next == Month(2000, 1))
}
"should have iterator for dates" in {
val dt = Month(2016, 6)
assert(dt.dates.size == 30)
val iter = for (v <- dt.dates) yield v
assert(iter.size == 30)
val dt2 = Month(2016, 2)
assert(dt2.dates.size == 29)
val dt3 = Month(1999, 2)
assert(dt3.dates.size == 28)
}
"should have iterator for hours" in {
val dt = Month(2016, 6)
assert(dt.hours.size == 30 * 24)
val iter = for (v <- dt.hours) yield v
assert(iter.size == 30 * 24)
}
}
"Year" - {
"should print date" in {
assert(Year(1999).toString == "1999")
assert(Year(2016).toString == "2016")
}
"should parse date" in {
val dt = Year.parse("2016")
assert(dt == Some(Year(2016)))
}
"should have next/prev" in {
val dt = Year(2016)
assert(dt.prev < dt)
assert(dt < dt.next)
assert(dt.prev == Year(2015))
assert(dt.next == Year(2017))
val dt2 = Year(1999)
assert(dt2.prev == Year(1998))
assert(dt2.next == Year(2000))
}
"should have iterator for dates" in {
val dt = Year(2016)
assert(dt.dates.size == 366)
val iter = for (v <- dt.dates) yield v
assert(iter.size == 366)
val dt2 = Year(1999)
assert(dt2.dates.size == 365)
}
"should have iterator for hours" in {
val dt = Year(2016)
assert(dt.hours.size == 366 * 24)
val iter = for (v <- dt.hours) yield v
assert(iter.size == 366 * 24)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment