Fix(?) align timeslots
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / accounting / dsl / Timeslot.scala
1 /*
2  * Copyright 2011 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
12  *   2. Redistributions in binary form must reproduce the above
13  *      copyright notice, this list of conditions and the following
14  *      disclaimer in the documentation and/or other materials
15  *      provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  *
30  * The views and conclusions contained in the software and
31  * documentation are those of the authors and should not be
32  * interpreted as representing official policies, either expressed
33  * or implied, of GRNET S.A.
34  */
35
36 package gr.grnet.aquarium.logic.accounting.dsl
37
38 import java.util.Date
39 import scala.collection.mutable
40 import annotation.tailrec
41 import gr.grnet.aquarium.util.date.MutableDateCalc
42
43 /**
44  * A representation of a timeslot with a start and end date.
45  *
46  * @author Georgios Gousios <gousiosg@gmail.com>
47  */
48 final case class Timeslot(from: Date, to: Date)
49   extends DSLItem with Ordered[Timeslot] {
50
51   /* Preconditions to ensure correct object creations */
52   assert(from != null)
53   assert(to != null)
54   assert(from.before(to), "from = %s, to = %s".format(new MutableDateCalc(from), new MutableDateCalc(to)))
55
56   def startsBefore(t: Timeslot) : Boolean = this.from.before(t.from)
57
58   def startsAfter(t: Timeslot) : Boolean = this.from.after(t.from)
59
60   def endsBefore(t: Timeslot) : Boolean = this.to.before(t.to)
61
62   def endsAfter(t: Timeslot) : Boolean = this.to.after(t.to)
63
64   def after(t: Timeslot): Boolean = if (this.from.after(t.to)) true else false
65
66   def before(t: Timeslot): Boolean = if (this.to.before(t.from)) true else false
67
68   /**
69    * Check whether this time slot fully contains the provided one.
70    */
71   def contains(t: Timeslot) : Boolean = {
72     if (this.from.getTime <= t.from.getTime &&
73       this.to.getTime >= t.to.getTime)
74       return true
75     return false
76   }
77
78   def containsTimeInMillis(millis: Long) = {
79     fromMillis <= millis && millis <= toMillis
80   }
81
82   /**
83    * Check whether this timeslot contains the provided time instant.
84    */
85   def includes(t: Date) : Boolean =
86     if (from.before(t) && to.after(t)) true else false
87
88   /**
89    * Check whether this timeslot overlaps with the provided one.
90    */
91   def overlaps(t: Timeslot) : Boolean = {
92     if (contains(t) || t.contains(this))
93       return true
94
95     if (this.includes(t.from) || this.includes(t.to))
96       return true
97
98     false
99   }
100
101   /**
102    * Merges this timeslot with the provided one. If the timeslots overlap,
103    * a list with the resulting merge is returned. If the timeslots do not
104      * overlap, the returned list contains both timeslots in increasing start
105    * date order.
106    */
107   def merge(t: Timeslot) : List[Timeslot] = {
108     if (overlaps(t)) {
109       val nfrom = if (from.before(t.from)) from else t.from
110       val nto   = if (to.after(t.to)) to else t.to
111       List(Timeslot(nfrom, nto))
112     } else
113       if (this.from.before(t.from))
114         List(this, t)
115       else
116         List(t, this)
117   }
118
119   /**
120    * Split the timeslot in two parts at the provided timestamp, if the
121    * timestamp falls within the timeslot boundaries.
122    */
123   def slice(d: Date) : List[Timeslot] =
124     if (includes(d))
125       List(Timeslot(from, d), Timeslot(d,to))
126     else
127       List(this)
128
129   /**
130    * Find and return the timeslots within which this Timeslot overrides
131    * with the provided list of timeslots. For example if:
132    * 
133    *  - `this == Timeslot(7,20)`
134    *  - `list == List(Timeslot(1,3), Timeslot(6,8), Timeslot(11,15))`
135    *
136    * the result will be: `List(Timeslot(7,8), Timeslot(11,15))`
137    */
138   def overlappingTimeslots(list: List[Timeslot]) : List[Timeslot] = {
139
140     val result = new mutable.ListBuffer[Timeslot]()
141
142     list.foreach {
143       t =>
144         if (t.contains(this)) result += this
145         else if (this.contains(t)) result += t
146         else if (t.overlaps(this) && t.startsBefore(this)) result += this.slice(t.to).head
147         else if (t.overlaps(this) && t.startsAfter(this)) result += this.slice(t.from).last
148     }
149     result.toList
150   }
151
152   /**
153    * Find and return the timeslots whithin which this Timeslot does not
154    * override with the provided list of timeslots. For example if:
155    *
156    *  - `this == Timeslot(7,20)`
157    *  - `list == List(Timeslot(1,3), Timeslot(6,8), Timeslot(11,15))`
158    *
159    * the result will be: `List(Timeslot(9,10), Timeslot(15,20))`
160    */
161   def nonOverlappingTimeslots(list: List[Timeslot]): List[Timeslot] = {
162
163     val overlaps = list.filter(t => this.overlaps(t))
164
165     if (overlaps.isEmpty)
166       return List(this)
167
168     def build(acc: List[Timeslot], listPart: List[Timeslot]): List[Timeslot] = {
169
170       listPart match {
171         case Nil => acc
172         case x :: Nil => build(acc, List())
173         case x :: y :: rest =>
174           build(acc ++ List(Timeslot(x.to,  y.from)), y :: rest)
175       }
176     }
177
178     val head = overlaps.head
179     val last = overlaps.reverse.head
180
181     val start = if (head.startsAfter(this)) List(Timeslot(this.from, head.from)) else List()
182     val end = if (last.endsBefore(this)) List(Timeslot(last.to, this.to)) else List()
183
184     start ++ build(List(), overlaps) ++ end
185   }
186
187   /**
188    * Align a list of consecutive timeslots to the boundaries
189    * defined by this timeslot. Elements that do not overlap
190    * with this timeslot are rejected, while elements not
191    * contained in the timeslot are trimmed to this timeslot's
192    * start and end time.
193    */
194   def align(l: List[Timeslot]): List[Timeslot] = {
195     if (l.isEmpty) return List()
196
197     val result : Option[Timeslot] =
198       if (!this.overlaps(l.head)) None
199       else if (l.head.contains(this)) Some(this)
200       else if (l.head.startsBefore(this)) Some(Timeslot(this.from, l.head.to))
201       else if (l.head.endsAfter(this)) Some(Timeslot(l.head.from, this.to))
202       else Some(this)
203
204     result match {
205       case Some(x) => x :: align(l.tail)
206       case None => align(l.tail)
207     }
208   }
209
210   /**
211    * Compares the starting times of two timeslots.
212    */
213   def compare(that: Timeslot): Int = {
214     if (this.startsBefore(that)) -1
215     else if (this.startsAfter(that)) 1
216     else 0
217   }
218
219   /**
220    * Converts the timeslot to the amount of hours it represents
221    */
222   def hours: Double = (to.getTime - from.getTime).toDouble / 1000.0 / 60.0 / 60.0
223
224   def deltaMillis = to.getTime - from.getTime
225
226   def fromMillis = from.getTime
227   def toMillis   = to.getTime
228
229   override def toString() = toDateString
230
231   def toDateString = "Timeslot(%s, %s)".format(new MutableDateCalc(from), new MutableDateCalc(to))
232   def toISODateString = "Timeslot(%s, %s)".format(new MutableDateCalc(from).toISOString, new MutableDateCalc(to).toISOString)
233 }
234
235 object Timeslot {
236   def apply(x: Long, y: Long): Timeslot =
237     new Timeslot(new Date(x), new Date(y))
238
239   def apply(x: Int, y: Int): Timeslot =
240     new Timeslot(new Date(x), new Date(y))
241 }