Reputation: 1403
I'm trying to create a simple api for dealing with intervals of hours. (I'm aware of joda time, and I'm not trying to reinvent it. This is rather an exercise).
What I would like to achieve is this:
(1)
assert(from("20:30").to("20:50") == Interval("20:30", "20:50") )
//same thing, but without implicit defs
assert(from(Time(20, 30)).to(Time(20, 50)) == Interval(Time(20, 30), Time(20, 50)))
assert(from("20:30").forMinutes(10) == from("20:30").to("20:40"))
I have managed to implement (1), like this: (ignoring toString, Ordered trait, a.s.o)
case class Time(hour: Int, minute: Int)
case class Interval(start: Time, end: Time)
object Interval {
case class HalfInterval(half: Time => Interval) {
def to(time: Time): Interval = half(time)
def forMinutes(minutes: Int): Interval = ???
}
def from(start: Time): HalfInterval = HalfInterval(Interval(start, _))
}
object Time {
def apply(hourMinute: String): Time = {
val tries = hourMinute.split(":").map(s => Try(s.toInt))
tries match {
case Array(Success(hour), Success(minute)) => Time(hour, minute)
case _ => throw new IllegalArgumentException
}
}
implicit def stringToTime(hourMinute: String) = Time(hourMinute)
}
However, I don't know how to implement (2) (that is: Interval.forMinutes).
def forMinutes(minutes: Int): Interval = {
val time = ?? // Based on what Time object could I construct a new Time object here?
half(time)
}
Can't seem to wrap my head around this.
Does this "HalfInterval" wrapper over Time => Interval
make sense at all?
I designed it empirically - just so that the from(..).to(..)
calls work as planned - rather than with some functional-conceptual model in mind.
Is there a better way to achieve this api?
Thanks
Upvotes: 1
Views: 416
Reputation: 8582
This is what I would do:
object Interval {
case class HalfInterval(start: Time) {
def to(end: Time): Interval = Interval(start, end)
def forMinutes(minutes: Int): Interval = to(getEnd(start, minutes))
private def getEnd(start: Time, minutes: Int) = ???
}
def from(start: Time): HalfInterval = HalfInterval(start)
}
getEnd() adds the minutes parameter to start.minute, divided by 60, adds the result to start.hours and the rest of the division to minutes and there you build the end Time. (Then maybe do the hour modulus 24 in case you go to the next day).
Edit: HalfInterval should be value class, but don't worry about that.
Upvotes: 3
Reputation: 39577
I agree with Luciano that your HalfInterval
is slightly out of the box.
But since engineers like things out-of-the-box, where you have to assemble it before you can play with it, here's another angle.
Your HalfInterval
says, give me an Interval
factory, and I'll hand you evidence about the Interval
that is required.
Here I've changed the param type to Any, but in this case it could be Either[Int, Time]
, where the int means minutes in the future and the time is an end time. More generally, it could be a marker trait, more safe than Any.
To answer your question, Where does the start time come from?, you see that the closure in from
captures the start
.
import scala.language.implicitConversions
import util._
case class Time(hour: Int, minute: Int) {
def +(delta: Int) = Time(hour, minute + delta) // TODO overflow minutes
}
case class Interval(start: Time, end: Time)
object Interval {
case class HalfInterval(half: Any => Interval) {
def to(time: Time): Interval = half(time)
def forMinutes(minutes: Int): Interval = half(minutes)
}
def from(start: Time) = HalfInterval((arg: Any) => arg match {
case delta: Int => Interval(start, start + delta)
case end: Time => Interval(start, end)
case _ => throw new IllegalArgumentException
})
}
object Time {
def apply(hourMinute: String): Time = {
val tries = hourMinute.split(":").map(s => Try(s.toInt))
tries match {
case Array(Success(hour), Success(minute)) => Time(hour, minute)
case _ => throw new IllegalArgumentException
}
}
implicit def stringToTime(hourMinute: String) = Time(hourMinute)
}
object Test extends App {
import Interval._
assert(from("20:30").to("20:50") == Interval("20:30", "20:50") )
assert(from("20:30").forMinutes(10) == from("20:30").to("20:40"))
}
An inverted formulation, where the signatures are stranger but the code makes more sense:
object Interval {
case class HalfInterval(f: (Time=>Time) => Interval) {
def to(end: Time): Interval = f(_ => end)
def forMinutes(minutes: Int): Interval = f(_ + minutes)
}
def from(start: Time) = HalfInterval((end: Time=>Time) => Interval(start, end(start)))
}
Both to
and forMinutes
know how to make an end time from a start time.
This makes it easier to add e.g. until(end: Time)
, forSeconds(secs: Int)
.
Upvotes: 1