Ruby-on-Rails-style date arithmetic for Scala
Last active
April 13, 2018 23:01
-
-
Save DarrenBishop/25d472722614786accf2 to your computer and use it in GitHub Desktop.
ChronoTime
This file contains 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
package com.darrenbishop.time | |
import java.util.Date | |
trait DateChrono { | |
implicit case object Chrono extends Chrono[Date] with ChronoString[Date] { | |
def parse(date: String): TimeType = new Date(date) | |
def toMillis(date: Date) = date.getTime | |
def fromMillis(millis: Long) = new Date(millis) | |
} | |
} | |
object DateChrono extends DateChrono |
This file contains 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
package com.darrenbishop.time | |
import org.joda.time.{DateTimeZone, DateTime} | |
trait DateTimeChrono { | |
implicit case object Chrono extends Chrono[DateTime] { | |
def toMillis(dateTime: DateTime) = dateTime.getMillis | |
def fromMillis(millis: Long) = new DateTime(millis, DateTimeZone.UTC) | |
} | |
} | |
object DateTimeChrono extends DateTimeChrono |
This file contains 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
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) | |
} | |
} | |
object LocalDateChrono extends LocalDateChrono |
This file contains 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
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) | |
} | |
} |
This file contains 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
package com.darrenbishop.time | |
trait SystemClock { | |
implicit case object clock extends Clock { | |
def millis = System.currentTimeMillis | |
} | |
} | |
object SystemClock extends SystemClock |
This file contains 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
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 |
This file contains 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
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