From fc6ecfe212c84271a3454617054aaf25890e886a Mon Sep 17 00:00:00 2001 From: Arthur Rand Date: Wed, 28 Mar 2018 05:56:21 -0700 Subject: [API-1468] add TimeOfDay (#141) * 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 --- src/main/scala/xyz/driver/core/generators.scala | 4 +- src/main/scala/xyz/driver/core/json.scala | 31 ++++++++- src/main/scala/xyz/driver/core/time.scala | 87 +++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) (limited to 'src/main/scala/xyz/driver') 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) -- cgit v1.2.3