aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArthur Rand <arand@ucsc.edu>2018-03-28 05:56:21 -0700
committerGitHub <noreply@github.com>2018-03-28 05:56:21 -0700
commitfc6ecfe212c84271a3454617054aaf25890e886a (patch)
tree4d6b85e059e10aa5b461af9e21c32c84bd73250f
parent30dba9ebf2abfd06452f46bac2b4c922043f56e6 (diff)
downloaddriver-core-fc6ecfe212c84271a3454617054aaf25890e886a.tar.gz
driver-core-fc6ecfe212c84271a3454617054aaf25890e886a.tar.bz2
driver-core-fc6ecfe212c84271a3454617054aaf25890e886a.zip
[API-1468] add TimeOfDay (#141)v1.8.11
* add TimeOfDay * add formatter * . * Revert "." This reverts commit 89576de98092dd75d3af7d82d244d5eaa24d31d9. * scalafmt * add before and after to ToD, and tests * rearrage, make fromStrings * add generator * address comments * use explicit string for TimeZoneId * renaming * revert Converters changes * change name of private method * change apply method * use month
-rw-r--r--src/main/scala/xyz/driver/core/generators.scala4
-rw-r--r--src/main/scala/xyz/driver/core/json.scala31
-rw-r--r--src/main/scala/xyz/driver/core/time.scala87
-rw-r--r--src/test/scala/xyz/driver/core/JsonTest.scala10
-rw-r--r--src/test/scala/xyz/driver/core/TimeTest.scala36
-rw-r--r--src/test/scala/xyz/driver/core/database/DatabaseTest.scala1
6 files changed, 165 insertions, 4 deletions
diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala
index e3ff326..143044c 100644
--- a/src/main/scala/xyz/driver/core/generators.scala
+++ b/src/main/scala/xyz/driver/core/generators.scala
@@ -3,7 +3,7 @@ package xyz.driver.core
import java.math.MathContext
import java.util.UUID
-import xyz.driver.core.time.{Time, TimeRange}
+import xyz.driver.core.time.{Time, TimeOfDay, TimeRange}
import xyz.driver.core.date.{Date, DayOfWeek}
import scala.reflect.ClassTag
@@ -69,6 +69,8 @@ object generators {
def nextTime(): Time = Time(math.abs(nextLong() % System.currentTimeMillis))
+ def nextTimeOfDay: TimeOfDay = TimeOfDay(java.time.LocalTime.MIN.plusSeconds(nextLong), java.util.TimeZone.getDefault)
+
def nextTimeRange(): TimeRange = {
val oneTime = nextTime()
val anotherTime = nextTime()
diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala
index 02a35fd..4d7fa04 100644
--- a/src/main/scala/xyz/driver/core/json.scala
+++ b/src/main/scala/xyz/driver/core/json.scala
@@ -1,7 +1,7 @@
package xyz.driver.core
import java.net.InetAddress
-import java.util.UUID
+import java.util.{TimeZone, UUID}
import scala.reflect.runtime.universe._
import scala.util.Try
@@ -14,7 +14,7 @@ import spray.json._
import xyz.driver.core.auth.AuthCredentials
import xyz.driver.core.date.{Date, DayOfWeek, Month}
import xyz.driver.core.domain.{Email, PhoneNumber}
-import xyz.driver.core.time.Time
+import xyz.driver.core.time.{Time, TimeOfDay}
import eu.timepit.refined.refineV
import eu.timepit.refined.api.{Refined, Validate}
import eu.timepit.refined.collection.NonEmpty
@@ -80,6 +80,33 @@ object json {
}
}
+ implicit object localTimeFormat extends JsonFormat[java.time.LocalTime] {
+ private val formatter = TimeOfDay.getFormatter
+ def read(json: JsValue): java.time.LocalTime = json match {
+ case JsString(chars) =>
+ java.time.LocalTime.parse(chars)
+ case _ => deserializationError(s"Expected time string got ${json.toString}")
+ }
+
+ def write(obj: java.time.LocalTime): JsValue = {
+ JsString(obj.format(formatter))
+ }
+ }
+
+ implicit object timeZoneFormat extends JsonFormat[java.util.TimeZone] {
+ override def write(obj: TimeZone): JsValue = {
+ JsString(obj.getID())
+ }
+
+ override def read(json: JsValue): TimeZone = json match {
+ case JsString(chars) =>
+ java.util.TimeZone.getTimeZone(chars)
+ case _ => deserializationError(s"Expected time zone string got ${json.toString}")
+ }
+ }
+
+ implicit val timeOfDayFormat: RootJsonFormat[TimeOfDay] = jsonFormat2(TimeOfDay.apply)
+
implicit val dayOfWeekFormat: JsonFormat[DayOfWeek] =
new EnumJsonFormat[DayOfWeek](DayOfWeek.All.map(w => w.toString -> w)(collection.breakOut): _*)
diff --git a/src/main/scala/xyz/driver/core/time.scala b/src/main/scala/xyz/driver/core/time.scala
index 3bcc7bc..bab304d 100644
--- a/src/main/scala/xyz/driver/core/time.scala
+++ b/src/main/scala/xyz/driver/core/time.scala
@@ -4,7 +4,10 @@ import java.text.SimpleDateFormat
import java.util._
import java.util.concurrent.TimeUnit
+import xyz.driver.core.date.Month
+
import scala.concurrent.duration._
+import scala.util.Try
object time {
@@ -39,6 +42,90 @@ object time {
}
}
+ /**
+ * Encapsulates a time and timezone without a specific date.
+ */
+ final case class TimeOfDay(localTime: java.time.LocalTime, timeZone: TimeZone) {
+
+ /**
+ * Is this time before another time on a specific day. Day light savings safe. These are zero-indexed
+ * for month/day.
+ */
+ def isBefore(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = {
+ toCalendar(day, month, year).before(other.toCalendar(day, month, year))
+ }
+
+ /**
+ * Is this time after another time on a specific day. Day light savings safe.
+ */
+ def isAfter(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = {
+ toCalendar(day, month, year).after(other.toCalendar(day, month, year))
+ }
+
+ def sameTimeAs(other: TimeOfDay, day: Int, month: Month, year: Int): Boolean = {
+ toCalendar(day, month, year).equals(other.toCalendar(day, month, year))
+ }
+
+ /**
+ * Enforces the same formatting as expected by [[java.sql.Time]]
+ * @return string formatted for `java.sql.Time`
+ */
+ def timeString: String = {
+ localTime.format(TimeOfDay.getFormatter)
+ }
+
+ /**
+ * @return a string parsable by [[java.util.TimeZone]]
+ */
+ def timeZoneString: String = {
+ timeZone.getID
+ }
+
+ /**
+ * @return this [[TimeOfDay]] as [[java.sql.Time]] object, [[java.sql.Time.valueOf]] will
+ * throw when the string is not valid, but this is protected by [[timeString]] method.
+ */
+ def toTime: java.sql.Time = {
+ java.sql.Time.valueOf(timeString)
+ }
+
+ private def toCalendar(day: Int, month: Int, year: Int): Calendar = {
+ val cal = Calendar.getInstance(timeZone)
+ cal.set(year, month, day, localTime.getHour, localTime.getMinute, localTime.getSecond)
+ cal
+ }
+ }
+
+ object TimeOfDay {
+ def now(): TimeOfDay = {
+ TimeOfDay(java.time.LocalTime.now(), TimeZone.getDefault)
+ }
+
+ /**
+ * Throws when [s] is not parsable by [[java.time.LocalTime.parse]], uses default [[java.util.TimeZone]]
+ */
+ def parseTimeString(tz: TimeZone = TimeZone.getDefault)(s: String): TimeOfDay = {
+ TimeOfDay(java.time.LocalTime.parse(s), tz)
+ }
+
+ def fromString(tz: TimeZone)(s: String): Option[TimeOfDay] = {
+ val op = Try(java.time.LocalTime.parse(s)).toOption
+ op.map(lt => TimeOfDay(lt, tz))
+ }
+
+ def fromStrings(zoneId: String)(s: String): Option[TimeOfDay] = {
+ val op = Try(TimeZone.getTimeZone(zoneId)).toOption
+ op.map(tz => TimeOfDay.parseTimeString(tz)(s))
+ }
+
+ /**
+ * Formatter that enforces `HH:mm:ss` which is expected by [[java.sql.Time]]
+ */
+ def getFormatter: java.time.format.DateTimeFormatter = {
+ java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")
+ }
+ }
+
object Time {
implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis)
diff --git a/src/test/scala/xyz/driver/core/JsonTest.scala b/src/test/scala/xyz/driver/core/JsonTest.scala
index a45025a..827624c 100644
--- a/src/test/scala/xyz/driver/core/JsonTest.scala
+++ b/src/test/scala/xyz/driver/core/JsonTest.scala
@@ -11,6 +11,7 @@ import xyz.driver.core.time.provider.SystemTimeProvider
import spray.json._
import xyz.driver.core.TestTypes.CustomGADT
import xyz.driver.core.domain.{Email, PhoneNumber}
+import xyz.driver.core.time.TimeOfDay
class JsonTest extends FlatSpec with Matchers {
import DefaultJsonProtocol._
@@ -61,6 +62,15 @@ class JsonTest extends FlatSpec with Matchers {
parsedTime should be(referenceTime)
}
+ "Json format for TimeOfDay" should "read and write correct JSON" in {
+ val utcTimeZone = java.util.TimeZone.getTimeZone("UTC")
+ val referenceTimeOfDay = TimeOfDay.parseTimeString(utcTimeZone)("08:00:00")
+ val writtenJson = json.timeOfDayFormat.write(referenceTimeOfDay)
+ writtenJson should be("""{"localTime":"08:00:00","timeZone":"UTC"}""".parseJson)
+ val parsed = json.timeOfDayFormat.read(writtenJson)
+ parsed should be(referenceTimeOfDay)
+ }
+
"Json format for Date" should "read and write correct JSON" in {
import date._
diff --git a/src/test/scala/xyz/driver/core/TimeTest.scala b/src/test/scala/xyz/driver/core/TimeTest.scala
index b83137c..b72fde8 100644
--- a/src/test/scala/xyz/driver/core/TimeTest.scala
+++ b/src/test/scala/xyz/driver/core/TimeTest.scala
@@ -7,6 +7,7 @@ import org.scalacheck.Prop.BooleanOperators
import org.scalacheck.{Arbitrary, Gen}
import org.scalatest.prop.Checkers
import org.scalatest.{FlatSpec, Matchers}
+import xyz.driver.core.date.Month
import xyz.driver.core.time.{Time, _}
import scala.concurrent.duration._
@@ -100,4 +101,39 @@ class TimeTest extends FlatSpec with Matchers with Checkers {
textualDate(EST)(timestamp) should not be textualDate(PST)(timestamp)
timestamp.toDate(EST) should not be timestamp.toDate(PST)
}
+
+ "TimeOfDay" should "be created from valid strings and convert to java.sql.Time" in {
+ val s = "07:30:45"
+ val defaultTimeZone = TimeZone.getDefault()
+ val todFactory = TimeOfDay.parseTimeString(defaultTimeZone)(_)
+ val tod = todFactory(s)
+ tod.timeString shouldBe s
+ tod.timeZoneString shouldBe defaultTimeZone.getID
+ val sqlTime = tod.toTime
+ sqlTime.toLocalTime shouldBe tod.localTime
+ a[java.time.format.DateTimeParseException] should be thrownBy {
+ val illegal = "7:15"
+ todFactory(illegal)
+ }
+ }
+
+ "TimeOfDay" should "have correct temporal relationships" in {
+ val s = "07:30:45"
+ val t = "09:30:45"
+ val pst = TimeZone.getTimeZone("America/Los_Angeles")
+ val est = TimeZone.getTimeZone("America/New_York")
+ val pstTodFactory = TimeOfDay.parseTimeString(pst)(_)
+ val estTodFactory = TimeOfDay.parseTimeString(est)(_)
+ val day = 1
+ val month = Month.JANUARY
+ val year = 2018
+ val sTodPst = pstTodFactory(s)
+ val sTodPst2 = pstTodFactory(s)
+ val tTodPst = pstTodFactory(t)
+ val tTodEst = estTodFactory(t)
+ sTodPst.isBefore(tTodPst, day, month, year) shouldBe true
+ tTodPst.isAfter(sTodPst, day, month, year) shouldBe true
+ tTodEst.isBefore(sTodPst, day, month, year) shouldBe true
+ sTodPst.sameTimeAs(sTodPst2, day, month, year) shouldBe true
+ }
}
diff --git a/src/test/scala/xyz/driver/core/database/DatabaseTest.scala b/src/test/scala/xyz/driver/core/database/DatabaseTest.scala
index f85dcad..8d2a4ac 100644
--- a/src/test/scala/xyz/driver/core/database/DatabaseTest.scala
+++ b/src/test/scala/xyz/driver/core/database/DatabaseTest.scala
@@ -39,5 +39,4 @@ class DatabaseTest extends FlatSpec with Matchers with Checkers {
an[DatabaseException] should be thrownBy TestConverter.expectValidOrEmpty(mapper, invalidOp)
}
-
}