Question

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)))


(2)

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

Was it helpful?

Solution

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.

OTHER TIPS

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).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top