Create a gist now

Instantly share code, notes, and snippets.

ChronoTime

Ruby-on-Rails-style date arithmetic for Scala

package com.darrenbishop.time
import java.util.Date
import scala.concurrent.duration.FiniteDuration
trait DateChrono {
implicit case object Chrono extends Chrono[Date] {
def toMillis(date: Date) = date.getTime
def fromMillis(millis: Long) = new Date(millis)
def -(date: Date, duration: FiniteDuration): Date = new Date(date.getTime - duration.toMillis)
def +(date: Date, duration: FiniteDuration): Date = new Date(date.getTime + duration.toMillis)
}
}
object DateChrono extends DateChrono
package com.darrenbishop.time
import org.joda.time.{DateTimeZone, DateTime}
import scala.concurrent.duration.FiniteDuration
trait DateTimeChrono {
implicit case object Chrono extends Chrono[DateTime] {
def toMillis(dateTime: DateTime) = dateTime.getMillis
def fromMillis(millis: Long) = new DateTime(millis, DateTimeZone.UTC)
def -(dateTime: DateTime, duration: FiniteDuration): DateTime = dateTime minus duration.toMillis
def +(dateTime: DateTime, duration: FiniteDuration): DateTime = dateTime plus duration.toMillis
}
}
object DateTimeChrono extends DateTimeChrono
package com.darrenbishop.time
import org.joda.time.LocalDate
import scala.concurrent.duration.FiniteDuration
trait LocalDateChrono {
implicit case object Chrono extends Chrono[LocalDate] with ChronoString[LocalDate] {
def parse(date: String) = LocalDate.parse(date)
def toMillis(localDate: LocalDate) = localDate.toDateTimeAtStartOfDay.getMillis
def fromMillis(millis: Long) = new LocalDate(millis)
def -(localDate: LocalDate, duration: FiniteDuration): LocalDate = localDate.toDateTimeAtStartOfDay minus duration.toMillis toLocalDate
def +(localDate: LocalDate, duration: FiniteDuration): LocalDate = localDate.toDateTimeAtStartOfDay plus duration.toMillis toLocalDate
}
}
object LocalDateChrono extends LocalDateChrono
package com.darrenbishop
import java.lang.Math.abs
import scala.language.implicitConversions
package object time {
import scala.concurrent.duration._
trait Clock {
def millis: Long
}
trait ChronoString[T] {
def parse(date: String): T
}
trait Chrono[T] {
def now(implicit clock: Clock) = fromMillis(clock.millis)
def toMillis(t: T): Long
def fromMillis(millis: Long): T
def -(t: T, duration: FiniteDuration): T
def +(t: T, duration: FiniteDuration): T
}
def parse[T](date: String)(implicit ev: ChronoString[T]): T = ev.parse(date)
def rightNow[T](implicit ev: Chrono[T], clock: Clock): T = ev.now
implicit class ChronoArithmetic[T](t: T)(implicit ev: Chrono[T]) {
def plus(duration: FiniteDuration) = ev.+(t, duration)
def +(duration: FiniteDuration) = plus(duration)
def minus(duration: FiniteDuration) = ev.-(t, duration)
def -(duration: FiniteDuration) = minus(duration)
def diff[T2](other: T2)(implicit ev2: Chrono[T2]): FiniteDuration = abs(ev.toMillis(t) - ev2.toMillis(other)) match {
case n if n % (1000 * 60 * 60 * 24) == 0 => (n / (1000 * 60 * 60 * 24)).days
case n if n % 1000 * 60 * 60 == 0 => (n / 1000 * 60 * 60).hours
case n if n % 1000 * 60 == 0 => (n / 1000 * 60).minutes
case n if n % 1000 == 0 => (n / 1000).seconds
case n => n.milliseconds
}
def -[T2](other: T)(implicit ev2: Chrono[T2]): FiniteDuration = diff(other)
}
implicit class RelativeFiniteDuration(duration: FiniteDuration) {
def from[T](t: T)(implicit ev :Chrono[T]): T = ev.+(t, duration)
def ahead[T](implicit ev: Chrono[T], clock: Clock): T = ev.+(ev.now, duration)
def ago[T](implicit ev: Chrono[T], clock: Clock): T = ev.-(ev.now, duration)
}
}
package com.darrenbishop.time
trait SystemClock {
implicit case object clock extends Clock {
def millis = {
System.currentTimeMillis
}
}
}
object SystemClock extends SystemClock
package com.darrenbishop.time
trait TestClock {
implicit case object clock extends Clock {
def millis = {
0 // since Unix Epoch, 1st January 1970
}
}
}
object TestClock extends TestClock
package com.darrenbishop.time
import org.scalatest.FlatSpec
import util.UnitSpec
class TimePackageSpec extends FlatSpec with UnitSpec {
import scala.concurrent.duration._
// We fix 'now' for all tests to Unix Epoch, 1st January 1970
trait PackageSpecContext extends TestClock
trait LocalDateFixtures extends TestClock with LocalDateChrono {
import org.joda.time.LocalDate
val laterDate = new LocalDate(1970, 1, 11)
}
"ChronoTime" should "add durations to Date via Chrono[Date]" in new PackageSpecContext {
import com.darrenbishop.time.DateChrono._
val sevenDaysLater = rightNow + 7.days
sevenDaysLater.getTime shouldBe 7 * 24 * 60 * 60 * 1000
}
it should "add durations to LocalDate via Chrono[LocalDate]" in new LocalDateFixtures {
val sevenDaysLater = rightNow + 7.days
sevenDaysLater.getDayOfMonth shouldBe 8
}
it should "add durations to DateTime via Chrono[DateTime]" in new PackageSpecContext {
import com.darrenbishop.time.DateTimeChrono._
val halfHourBefore = rightNow - 30.minutes
halfHourBefore.getYear shouldBe 1969
}
it should "give the largest unit duration when diff-ing dates" in new LocalDateFixtures {
laterDate - rightNow should be(10.days)
rightNow - laterDate should be(10.days)
}
it can "create dates from parseable strings, via ChronoString[LocalDate]" in new LocalDateFixtures {
val date = parse("1970-01-11")
date.getYear shouldBe 1970
date.getMonthOfYear shouldBe 01
date.getDayOfMonth shouldBe 11
}
it can "create dates in the future, given a duration" in new PackageSpecContext {
import com.darrenbishop.time.DateChrono._
val sevenDaysLater = 7.days.ahead
sevenDaysLater.getTime shouldBe 7 * 24 * 60 * 60 * 1000
}
it can "create dates in the past, given a duration" in new PackageSpecContext {
import com.darrenbishop.time.DateTimeChrono._
val twentyDaysBefore = 20.days.ago
twentyDaysBefore.getYear shouldBe 1969
twentyDaysBefore.getMonthOfYear shouldBe 12
twentyDaysBefore.getDayOfMonth shouldBe 12
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment