Last active
June 8, 2016 14:11
-
-
Save jonas/08605295e7a12fbffc469001293278e0 to your computer and use it in GitHub Desktop.
WIP porting Luigi's DateInterval to Scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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