interval-algebra foundations
Introduction
Scope of this tutorial
This tutorial covers the IntervalAlgebra.Core and IntervalAlgebra.PairedInterval modules.
Theoretical background
The interval-algebra package implements Allen’s interval algebra as defined in
Allen (1983) and axiomatized in
Allen and Hayes (1987).
A good primer on Allen’s algebra can be
found here.
Imports used during examples
The following import declarations are used for the examples provided in this documentation.
import IntervalAlgebra
import IntervalAlgebra.IntervalDiagram
import Data.Bifunctor ( Bifunctor(..) )
import Data.List ( sort )
import Data.Set ( Set
, difference
, fromList
)
import Data.Time ( Day
, UTCTime(..)
, addDays
, fromGregorian
, secondsToDiffTime
)
import Witch ( into )
Intervals
The Interval type
An interval in time according to Allen’s interval algebra is defined by a starting point and an ending point such that the start occurs before the end.
In particular, the start and end cannot be the same moment in time.
Intervals in interval-algebra are modeled using the Interval type shown below.
Note that in order to ensure the starting point for an Interval always occurs before the ending point,
interval-algebra does not export the constructor for Interval, and instead provides a collection of functions that can be used to create Intervals that are guaranteed to maintain the invariant.
data Interval a
Displaying Intervals using 'show'
The show method displays the interval endpoints using the form (<starting point>, <ending point>).
We haven’t actually described how the following intervals were created yet; these particular variables are created in the Creating Intervals using 'safeInterval' section.
-- An example Interval Integer
print ivInteger
---> (2, 6)
-- An example Interval Day
print ivDay
---> (1967-01-18, 1967-01-24)
-- An example Interval UTCTime
print ivUTC
---> (1967-01-18 09:00:00 UTC, 1967-01-18 09:13:20 UTC)
Basic Interval instances
The following are some of the basic instances implemented by Interval.
Eq a => Eq (Interval a)
Ord a => Ord (Interval a)
instance (Show a, Ord a) => Show (Interval a)
print $ ivInteger == ivInteger
---> True
print $ ivDay < ivDay
---> False
print $ show ivInteger
---> "(2, 6)"
Creating Intervals
The available functions are listed in the following table.
The function name is listed in the first column, a short alias name is listed in the second column,
and the return type is listed in the third column
(note that there may be constraints imposed on the type a that are not shown here).
The parseInterval construction function has the type constraint Ord a for the type parameter a in the return type.
This is required since in order to ensure that a valid interval is being created we need to ensure that the starting point is less than the ending point in the sense of the < operator.
The other Interval construction functions all implicitly require Ord a as well as another constraint that we won’t describe at this time since we don’t introduce the relevant type class until the The IntervalSizeable class section.
For now, it suffices to know that the Int, Integer, Data.Time.Day,
and Data.Time.UTCTime types all implement the necessary classes in order to create an Interval using any of the functions described in this section.
| Name | Alias | Return Type |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Creating Maybe Intervals using 'parseInterval'
For the parseInterval function we provide a left endpoint and a right endpoint as inputs.
If the left endpoint is strictly less than the right endpoint then we get a Right (Interval a),
otherwise we get a Left ParseErrorInterval.
rightIvInteger :: Either ParseErrorInterval (Interval Integer)
rightIvInteger = parseInterval 0 2
leftIvInteger :: Either ParseErrorInterval (Interval Integer)
leftIvInteger = parseInterval 2 2
rightIvDay :: Either ParseErrorInterval (Interval Day)
rightIvDay =
parseInterval (fromGregorian 1967 01 18) (fromGregorian 1967 01 22)
rightIvUTC :: Either ParseErrorInterval (Interval UTCTime)
rightIvUTC = parseInterval
(UTCTime (fromGregorian 1967 01 18) (secondsToDiffTime 32400))
(UTCTime (fromGregorian 1967 01 18) (secondsToDiffTime 33200))
print rightIvInteger
---> Right (0, 2)
print leftIvInteger
---> Left (ParseErrorInterval "2<=2")
print rightIvDay
---> Right (1967-01-18, 1967-01-22)
print rightIvUTC
---> Right (1967-01-18 09:00:00 UTC, 1967-01-18 09:13:20 UTC)
Creating Intervals using 'safeInterval'
For the safeInterval function we provide a pair with a starting point and ending point as the input.
If the ending point is no greater than the starting point then the ending point is adjusted so that the duration is the minimal allowed amount for the endpoint type as defined by the moment method for the IntervalSizeable class
(see The IntervalSizeable class section for details),
while the starting point is left unchanged.
This mechanism ensures that an Interval is created where the starting point is strictly less than the ending point.
For example, the minimum duration allowed for an Interval Integer is 1.
In one of the examples below, the duration provided for ivMinDurInteger is less than 1 so consequently the ending point for the resulting Interval is adjusted to be 1 more than the starting point.
ivInteger :: Interval Integer
ivInteger = safeInterval (2, 6)
ivMinDurInteger :: Interval Integer
ivMinDurInteger = safeInterval (2, 2)
ivDay :: Interval Day
ivDay = safeInterval (fromGregorian 1967 01 18, fromGregorian 1967 01 24)
ivUTC :: Interval UTCTime
ivUTC = safeInterval
( UTCTime (fromGregorian 1967 01 18) (secondsToDiffTime 32400)
, UTCTime (fromGregorian 1967 01 18) (secondsToDiffTime 33200)
)
print ivInteger
---> (2, 6)
print ivMinDurInteger
---> (2, 3)
print ivInteger
---> (2, 6)
print ivDay
---> (1967-01-18, 1967-01-24)
print ivUTC
---> (1967-01-18 09:00:00 UTC, 1967-01-18 09:13:20 UTC)
Creating Intervals using 'beginerval' and 'enderval'
The beginerval and enderval functions offer alternatives to the safeInterval function for creating Intervals.
The beginerval function takes a duration and a starting point as inputs,
while the enderval function takes a duration and an ending point as inputs.
Similar to how the safeInterval function operates,
if the duration is smaller than the minimal allowed amount for the endpoint type as defined by the moment method for the IntervalSizeable class,
then the duration is adjusted to take the value of the minimum duration.
In the following examples, we see two instances where the input duration is smaller than 1, which is the minimal allowed amount for an Interval Integer. Note that the expression type signatures are needed to prevent an ambiguous type variable error.
print (beginerval 2 3 :: Interval Integer)
---> (3, 5)
print (beginerval (-2) 3 :: Interval Integer)
---> (3, 4)
print (enderval 2 12 :: Interval Integer)
---> (10, 12)
print (enderval (-2) 12 :: Interval Integer)
---> (11, 12)
Creating Intervals using 'beginervalMoment' and 'endervalMoment'
The beginervalMoment and endervalMoment functions create Intervals with the minimum duration as defined by the corresponding IntervalSizeable instance.
In the case of beginervalMoment the starting point is specified by the function’s input value,
while for the case of endervalMoment the ending point is specified by the function’s input value.
print (beginervalMoment 11 :: Interval Integer)
---> (11, 12)
print (endervalMoment 11 :: Interval Integer)
---> (10, 11)
Additional Interval data
The following data will be used in various places in the study.
The naming convention used for the variables is as follows:
each has type Interval Integer,
and a given variable with a name like e.g. iv6to8 has a starting point of 6 and an ending point of 8.
iv0to2, iv2to4, iv2to5, iv4to5, iv5to8, iv6to8, iv3to6 :: Interval Integer
iv0to2 = safeInterval (0, 2)
iv2to4 = safeInterval (2, 4)
iv2to5 = safeInterval (2, 5)
iv3to6 = safeInterval (3, 6)
iv4to5 = safeInterval (4, 5)
iv5to8 = safeInterval (5, 8)
iv6to8 = safeInterval (6, 8)
PairedIntervals
Allen’s interval algebra provides the theoretical underpinnings of the interval-algebra library, and is defined exclusively with regards to intervals. However, applications typically aren’t solely interested in the relationships between abstract intervals in time, and instead usually care about what happened as well as when it happened. For example:
-
What was the first event with starting point no earlier than January 1, 2000 at 00:00:00?
-
What were the events that occurred with starting point no earlier than 0 and ending point no later than 455?
To support these kinds of needs interval-algebra provides the PairedInterval type.
The PairedInterval type
The PairedInterval type is defined as follows. The constructor is not exported.
data PairedInterval b a
You can create a PairedInterval using the makePairedInterval function which has the following type signature.
makePairedInterval :: b -> Interval a -> PairedInterval b a
The following are some examples of creating PairedIntervals.
The show method displays PairedIntervals using the form {<interval>, <data>}.
pairListstringInteger :: PairedInterval [String] Integer
pairListstringInteger =
makePairedInterval ["John", "Paul", "George", "Ringo"] ivInteger
pairStringDay :: PairedInterval String Day
pairStringDay = makePairedInterval "vacation" ivDay
print pairListstringInteger
---> {(2, 6), ["John","Paul","George","Ringo"]}
print pairStringDay
---> {(1967-01-18, 1967-01-24), "vacation"}
Basic PairedInterval instances
The following are some of the basic instances implemented by PairedInterval.
(Eq a, Eq b) => Eq (PairedInterval b a)
(Eq a, Eq b, Ord a) => Ord (PairedInterval b a)
(Show b, Show a, Ord a) => Show (PairedInterval b a)
print $ pairStringDay == pairStringDay
---> True
print $ pairListstringInteger < pairListstringInteger
---> False
print $ show pairStringDay
---> "{(1967-01-18, 1967-01-24), \"vacation\"}"
Getting and setting the elements of a PairedInterval
Getting and setting the interval portion of a PairedInterval
The ability to get and set the Interval portion of a PairedInterval is provided by getInterval and setInterval methods of the Intervallic class along with the associated helper functions begin and end.
You can also use the intervals convenience function for extracting the intervals out of a Functor of PairedIntervals.
See the The PairedInterval instance of Intervallic section for more details regarding the type class and instance.
print pairListstringInteger
---> {(2, 6), ["John","Paul","George","Ringo"]}
print $ getInterval pairListstringInteger
---> (2, 6)
print $ setInterval pairListstringInteger (safeInterval (4, 9) :: Interval Integer)
---> {(4, 9), ["John","Paul","George","Ringo"]}
print $ intervals [pairListstringInteger, pairListstringInteger]
---> [(2, 6),(2, 6)]
print $ begin pairListstringInteger
---> 2
print $ end pairListstringInteger
---> 6
Getting and setting the data portion of a PairedInterval
To get the data (i.e. the non-interval) portion out of a PairedInterval value you can use the getPairData function.
To set the data you have to create a new PairedInterval with the updated data.
print pairStringDay
---> {(1967-01-18, 1967-01-24), "vacation"}
print $ getPairData pairStringDay
---> "vacation"
print $ makePairedInterval "ski trip" (getInterval pairStringDay)
---> {(1967-01-18, 1967-01-24), "ski trip"}
Interfaces for embedding Intervals
interval-algebra is built around three typeclasses designed to separate concerns of constructing,
relating, and combining types that contain Intervals:
-
Intervallicprovides an interface to the data structures which contain anInterval. -
IntervalCombinableprovides an interface to methods of combining twoIntervals. -
IntervalSizeableprovides methods for measuring and modifying the size of an interval.
An advantage of nested typeclass design is that users can define a types with embedded Intervals with just the amount of structure that they need.
The Intervallic class
The Intervallic class is defined as
class Intervallic i where
with methods
getInterval :: i a -> Interval a
setInterval :: i a -> Interval b -> i b
and accompanying helper functions for getting the starting point and ending point out of an Interallic value:
begin :: Intervallic i => i a -> a
end :: Intervallic i => i a -> a
These methods provide an interface for classes to define a way to get an Interval a out of an i a,
and to change the value of an Interval a contained in an i a.
The Interval instance of Intervallic
The following instance is provided for Intervallic:
instance Intervallic Interval
and the methods specialize to
getInterval :: Interval a -> Interval a
setInterval :: Interval a -> Interval b -> Interval b
For the case of getInterval the Interval provided as the input is returned unchanged,
while for the case of setInterval the replacement Interval (i.e. the second input) is returned unchanged.
print ivInteger
---> (2, 6)
print $ getInterval ivInteger
---> (2, 6)
print $ setInterval ivInteger (beginerval 3 12 :: Interval Integer)
---> (12, 15)
print $ begin ivInteger
---> 2
print $ end ivInteger
---> 6
Obviously this is not very useful for Interval values.
Where Intervallic comes in handy is for embedding Intervals in other types — as long as we know how to extract and to replace the embedded interval we can do all of the usual interval-algebra operations that we would do to regular Intervals.
The PairedInterval instance of Intervallic
The following instance is provided for Intervallic:
instance Intervallic (PairedInterval b)
and the methods specialize to
getInterval :: PairedInterval b a -> Interval a
setInterval :: PairedInterval b a -> Interval c -> PairedInterval b c
See the Getting and setting the interval portion of a PairedInterval section for example usages of Intervallic methods.
The IntervalSizeable class
The IntervalSizeable class is defined as
class (Ord a, Num b, Ord b) => IntervalSizeable a b | a -> b
with methods
moment :: forall a . b
duration :: Intervallic i => i a -> b
add :: b -> a -> a
diff :: a -> a -> b
In general, for a pair of types a and b, the IntervalSizeable class provides an interface for operations related to calculating the duration of an existing Intervallic and other size-related concerns.
For the important special case of the Interval instance of Intervallic, the a type represents the type used for the starting and ending points of the Interval,
while the b type for the interval-algebra defined instances of IntervalSizeable represents the type obtained by calculating the "difference" between the starting and ending points,
as defined by diff.
User-defined instances of Intervallic need not maintain this interpretation.
IntervalSizeable instances
The following instances are provided for IntervalSizeable.
IntervalSizeable Int Int
IntervalSizeable Integer Integer
IntervalSizeable Day Integer
IntervalSizeable UTCTime NominalDiffTime
As an example, consider the IntervalSizeable Day Integer instance with the following methods.
moment :: forall a. Integer
duration :: Intervallic i Day => i Day -> Integer
add :: Integer -> Day -> Day
diff :: Day -> Day -> Integer
In the event that we apply duration to an Interval Day then we get a further specialization of duration to the following type.
duration :: Interval Day -> Integer
The following are some example uses of IntervalSizeable methods.
print ivDay
---> (1967-01-18, 1967-01-24)
print $ moment @Day
---> 1
print $ duration ivDay
---> 6
print $ add 15 (begin ivDay)
---> 1967-02-02
print $ diff (add 15 (begin ivDay)) (begin ivDay)
---> 15
The IntervalCombinable class
The IntervalCombinable class is defined as
class Intervallic i => IntervalCombinable i a
with methods
(.+.) :: i a -> i a -> Maybe (i a)
(><) :: i a -> i a -> Maybe (i a)
(<+>) :: (Semigroup (f (i a)), Applicative f) => i a -> i a -> f (i a)
The Interval instance of IntervalCombinable
Method types
The following instance (among others) is provided for IntervalCombinable:
Ord a => IntervalCombinable Interval a
and the methods specialize to
(.+.) :: Ord a => Interval a -> Interval a -> Maybe (Interval a)
(><) :: Ord a => Interval a -> Interval a -> Maybe (Interval a)
(<+>) :: (Ord a, Semigroup (f (Interval a)), Applicative f) =>
Interval a -> Interval a -> f (Interval a)
A brief introduction to 'meets' and 'before'
We don’t properly introduce the meets and before functions that are used by the IntervalCombinable methods until the upcoming The IntervalRelation type and associated predicates section.
But for now it suffices to know that for two Intervals x and y:
-
The result of evaluating
x `meets` yisTrueif the ending point ofxis equal to the starting point ofy, andFalseotherwise. -
The result of evaluating
x `before` yisTrueif the ending point ofxis less than the starting point ofy, andFalseotherwise.
IntervalCombinable methods for Interval
-
The
.+.method returns aJustvalue if the first inputmeetsthe second, andNothingotherwise. In the case that the value is aJustthen it contains theIntervalcreated by combining the two inputs, or more precisely stated it contains theIntervalwith the starting point given by the starting point of the first input, and the ending point given by the ending point of the second input. -
The
><method returns aJustvalue if the first input isbeforethe second, andNothingotherwise. In the case that the value is aJustthen it contains theIntervalcreated by spanning the two inputs, or more precisely stated it contains theIntervalwith the starting point given by the ending point of the first input, and the ending point given by the starting point of the second input. -
The
<+>method is a little bit more complicated to reason about since its behavior differs depending on the return type.-
If we have two inputs where the first input is
beforethe second, then it lifts the inputs to the return type and combines them by using the<>method. -
If we have two inputs where the first input is not
beforethe second, then it creates a lifted version of anIntervalwith regards to the output type, and where the starting point is the earlier of the two starting points and the ending point is the later of the two ending points.
-
The following are some example usages of these methods.
Recall from the Additional Interval data section that a variable with a name like e.g. iv2to5 has a starting point of 2 and an ending point of 5.
-- The Just Interval formed from combining the Intervals, since iv0to2 `meets` iv2to5
print $ iv0to2 .+. iv2to5
---> Just (0, 5)
-- A Nothing since iv0to2 doesn't `meets` iv5to8
print $ iv0to2 .+. iv5to8
---> Nothing
-- The Just Interval formed from the end of the first and the beginning of the
-- second, since iv0to2 is `before` iv5to8
print $ iv0to2 >< iv5to8
---> Just (2, 5)
-- A Nothing since iv0to2 isn't `before` iv2to5
print $ iv0to2 >< iv2to5
---> Nothing
The PairedInterval instance of IntervalCombinable
The following instance (among others) is provided for IntervalCombinable:
instance (Ord a, Eq b, Monoid b) => IntervalCombinable (PairedInterval b) a
and the methods specialize to
(.+.) :: (Ord a, Eq b, Monoid b) =>
PairedInterval b a -> PairedInterval b a -> Maybe (PairedInterval b a)
(><) :: (Ord a, Eq b, Monoid b) =>
PairedInterval b a -> PairedInterval b a -> Maybe (PairedInterval b a)
(<+>) :: ((Ord a, Eq b, Monoid b), Semigroup (f (PairedInterval b a)), Applicative f) =>
PairedInterval b a -> PairedInterval b a -> f (PairedInterval b a)
In what follows we describe the IntervalCombinable methods for PairedInterval and provide some examples.
Note that we don’t properly introduce the meets and before functions that are used by the IntervalCombinable methods until the upcoming The IntervalRelation type and associated predicates section,
but for a short introduction see the A brief introduction to 'meets' and 'before' section.
-
The
.+.method returns aJustvalue if the first inputmeetsthe second, andNothingotherwise. In the case that the value is aJustthen it contains aPairedIntervalwith the data portion given by the data in the second argument to.+., and theIntervalportion obtained by combining the two inputs (more precisely stated it contains theIntervalwith the starting point given by the starting point of the first input, and the ending point given by the ending point of the second input). -
The
><method returns aJustvalue if the first input isbeforethe second, andNothingotherwise. In the case that the value is aJustthen it contains thePairedIntervalwith the data portion given by thememptymethod for theMonoidinstance of the data type ofPairedIntervals, and theIntervalportion obtained by spanning the two inputs (or more precisely stated it contains theIntervalwith the starting point given by the ending point of the first input, and the ending point given by the starting point of the second input). -
The
<+>method is a little bit more complicated to reason about since its behavior differs depending on the return type.-
If we have two inputs where the first input is
beforethe second, then it lifts the inputs to the return type and combines them by using the<>method. -
If we have two inputs where the first input is not
beforethe second, then it creates a lifted version of aPairedIntervalwith regards to the output type, where the data is the result of applying<>to the data portions of the two inputs, and where theIntervalis such that the starting point is the earlier of the two starting points of the embeddedIntervals and the ending point is the later of the two ending points of the embeddedIntervals.
-
The following are some example usages of these methods.
Recall from the Additional Interval data section that a variable with a name like e.g. iv2to5 has a starting point of 2 and an ending point of 5.
-- The Just Interval formed from combining the Intervals and taking the data
-- portion from the second argument, since iv0to2 `meets` iv2to5
print $ makePairedInterval "a" iv0to2 .+. makePairedInterval "b" iv2to5
---> Just {(0, 5), "b"}
-- A Nothing since iv0to2 doesn't `meets` iv5to8
print $ makePairedInterval "a" iv0to2 .+. makePairedInterval "b" iv5to8
---> Nothing
-- The Just Interval formed from spanning the Intervals and taking the data
-- portion from the `mempty` method of the Monoid String instance, since
-- iv0to2 is `before` iv5to8
print $ makePairedInterval "a" iv0to2 >< makePairedInterval "b" iv5to8
---> Just {(2, 5), ""}
-- A Nothing since iv0to2 isn't `before` iv2to5
print $ makePairedInterval "a" iv0to2 >< makePairedInterval "b" iv2to5
---> Nothing
Updating intervals
Expanding Intervallic endpoints
Interval-algebra provides a collection of functions for expanding one or more Intervallic endpoints,
and where in this context "expanding" means that the endpoints can only be updated in a direction that makes the duration of the Intervallic larger.
-
expandlmoves anIntervallic's starting point "to the left." The inputs are a duration and anIntervallicvalue. -
expandrmoves anIntervallic's ending point "to the right." The inputs are a duration and anIntervallicvalue. -
expandmoves anIntervallic's starting point "to the left" and ending point "to the right." The inputs are two durations and anIntervallicvalue.
If the provided value by which to move either the starting or ending points is less than the minimal amount,
as defined by the moment method for the corresponding IntervalSizeable class,
then the corresponding point is left unchanged.
This has the effect of ensuring that these methods cannot decrease the duration of the Interval,
as all sensible IntervalSizeable instances will have a positive moment.
print ivInteger
---> (2, 6)
print $ expandl 4 ivInteger
---> (-2, 6)
print $ expandl 0 ivInteger
---> (2, 6)
print $ expandr 5 ivInteger
---> (2, 11)
print $ expandr (-3) ivInteger
---> (2, 6)
print $ expand 4 5 ivInteger
---> (-2, 11)
print $ expand 0 (-3) ivInteger
---> (2, 6)
Creating intervals that share an endpoint with an Intervallic
The following functions can be used to create an Interval that shares an endpoint with an Intervallic.
-
beginervalFromEndcreates an interval that has the same starting endpoint as the ending point of an inputIntervallicvalue. The inputs are a duration and anIntervallicvalue. -
endervalFromBegincreates an interval that has the same ending point as the starting point of an inputIntervallicvalue. The inputs are a duration and anIntervallicvalue. -
momentizecreates an interval with the minimum duration for the correspondingIntervalSizeableinstance that has the same starting point as the starting point of an inputIntervallicvalue. The input is anIntervallicvalue.
In the event that any of the provided durations are less than the minimal allowed amount for the endpoint type as defined by the moment method for the corresponding IntervalSizeable class then the duration is adjusted to be the mimimum duration.
print ivInteger
---> (2, 6)
beginervalFromEnd 5 ivInteger
---> (6, 11)
beginervalFromEnd (-2) ivInteger
---> (6, 7)
endervalFromBegin 12 ivInteger
---> (-10, 2)
endervalFromBegin (-6) ivInteger
---> (1, 2)
print $ momentize ivInteger
---> (2, 3)
Shifting intervals
The shiftFromBegin and shiftFromEnd functions have the following type signatures.
shiftFromBegin
:: (IntervalSizeable a b, Intervallic i0 a)
=> i0 a -> i1 a -> i1 b
shiftFromEnd
:: (IntervalSizeable a b, Intervallic i0 a)
=> i0 a -> i1 a -> i1 b
For each function, you’re creating a new interval with starting and ending points given in units relative to the starting point (in the case of ShiftFromBegin) or ending point (in the case of ShiftFromEnd) of another Intervallic.
This can be useful when you don’t care about the abolute time of when something happened, but rather when it happened relative to some other event.
In more detail, shiftFromBegin and shiftFromEnd have the following behavior:
-
shiftfromBegindiffs the starting point from the first input from both the starting and ending point of the second input. -
shiftfromEndis similar toshiftfromBeginexcept that it applies thediffto the ending point of its first input.
In the examples below we see various combinations of types that can be provided as inputs for shiftFromBegin and shiftFromEnd.
Recall from the Additional Interval data section that a variable with a name like e.g. iv2to4 has a starting point of 2 and an ending point of 4.
Also note that the return type need not be the same as the input types, as is the case when the inputs are Interval Days.
print [iv2to4, iv5to8]
---> [(2, 4),(5, 8)]
print ivDay
---> (1967-01-18, 1967-01-24)
print $ shiftFromBegin iv2to4 iv5to8
---> (3, 6)
print $ shiftFromBegin ivDay ivDay
---> (0, 6)
print $ shiftFromEnd iv2to4 iv5to8
---> (1, 4)
print $ shiftFromEnd ivDay ivDay
---> (-6, 0)
Interval relations
The ComparativePredicateOf1 and ComparativePredicateOf2 types
The following are the type declarations for ComparativePredicateOf1 and ComparativePredicateOf2;
these will be needed for the upcoming The IntervalRelation type and associated predicates section.
type ComparativePredicateOf1 a = (a -> a -> Bool)
type ComparativePredicateOf2 a b = (a -> b -> Bool)
The IntervalRelation type and associated predicates
See Thirteen basic relations for a figure corresponding to each of the possible interval relations.
The following type enumerates each of the possible relationships that a pair of Intervallic values can have.
The name in the comment to the right of each type is a predicate function corresponding to the particular variant through the relate function,
in the sense that if the value returned by x `relate` y is e.g. Starts,
then x `starts` y evaluates to True.
data IntervalRelation =
Before -- before/precedes
| Meets -- meets
| Overlaps -- overlaps
| FinishedBy -- finishedBy
| Contains -- contains
| Starts -- starts
| Equals -- equals
| StartedBy -- startedBy
| During -- during
| Finishes -- finishes
| OverlappedBy -- overlappedBy
| MetBy -- metBy
| After -- after/precededBy
Next, consider the following diagram providing a visual representation of some example data.
This diagram is created using the parseIntervalDiagram,
however in order to save space we do not provide the definition used to create the diagram.
Each row shows an interval represented using hyphens and corresponding to a variable with the name displayed on the right-hand side of the row.
Recall from the Additional Interval data section that a variable with a name like e.g. iv2to5 has a starting point of 2 and an ending point of 5.
The hyphens in the diagram corresponding to e.g. iv2to5 start after the second character and end after the fifth character.
-- <- [iv0to2]
-- <- [iv2to4]
--- <- [iv2to5]
--- <- [iv3to6]
- <- [iv4to5]
-- <- [iv6to8]
--- <- [iv5to8]
========
The following table describes the 13 predicates corresponding to the 13 Interval Algebra relations. Each of the predicates has the following type signature.
(Ord a, Intervallic i0, Intervallic i1) => ComparativePredicateOf2 (i0 a) (i1 a)
| Name | Definition | Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Also note that there are functions before and after which are synonyms for precedes and precededBy,
respectively.
Composite relations and predicates
The fundamental interval algebra relations are rather fine-grained, and often the relationships we would like to use to describe pairs of intervals are compositions of the fundamental relations. For this reason, interval-algebra provides some built-in relations composed of multiple fundamental relations, as well as facilities for defining your own composite relations.
Built-in composite relations
The variables in the following table each have the type Set IntervalRelation.
| Name | Relations |
|---|---|
|
|
|
|
|
|
|
Every fundamental relation |
Additional points of note:
-
disjointRelationscorresponds to thedisjointpredicate -
withinRelationscorresponds to theenclosedBy/withinpredicates -
strictWithinRelationsdoesn’t currently correspond to any built-in predicates -
intervalRelationsis useful if you want to create a large composite relation by removing a few relations (but also seecomplement)
Built-in composite predicates
| Name | Definition |
|---|---|
|
Does one of the relations hold: |
|
Do none of the relations hold: |
|
Do none of the relations hold: |
|
Does one of the relations hold: |
|
Does one of the relations hold: |
|
Does one of the relations hold: |
Additional points of note:
-
The
disjointandnotDisjoint/concurpredicates are complements of each other in the sense thatx `disjoint` yandnot (x `notDisjoint` y)evalute to the same value for any pair ofxandy. -
The
enclosesandenclosedBy/withinpredicates are duals of each other in the sense thatx `encloses` yandy `enclosedBy` xevalute to the same value for any pair ofxandy.
Constructing custom composite relations
The following examples demonstrate some approaches for constructing Set IntervalRelations.
-- Set, `fromList`, and in a later example `difference` are imported from
-- Data.Set
endedPriorRelations :: Set IntervalRelation
endedPriorRelations = fromList [Before, Meets]
-- We can in general create a new Set by taking the set Difference of one Set
-- and another Set
notEndedPriorRelations :: Set IntervalRelation
notEndedPriorRelations = intervalRelations `difference` endedPriorRelations
-- However, the `complement` function is provided for the common case of taking
-- the Set Difference of the `intervalRelations` Set and another Set
notEndedPriorRelations' :: Set IntervalRelation
notEndedPriorRelations' = complement endedPriorRelations
-- IntervalAlgebra exports versions of `Data.Set`s `intersection` and `union`
-- functions where the types are specialized to `Set IntervalRelation`s
intervalRelations' :: Set IntervalRelation
intervalRelations' = endedPriorRelations `union` notEndedPriorRelations
-- The intersection of two disjoint sets
empty :: Set IntervalRelation
empty = endedPriorRelations `intersection` notEndedPriorRelations
print endedPriorRelations
---> fromList [Before,Meets]
print notEndedPriorRelations
---> fromList [Overlaps,FinishedBy,Contains,Starts,Equals,StartedBy,During,Finishes,OverlappedBy,MetBy,After]
print notEndedPriorRelations'
---> fromList [Overlaps,FinishedBy,Contains,Starts,Equals,StartedBy,During,Finishes,OverlappedBy,MetBy,After]
print intervalRelations'
---> fromList [Before,Meets,Overlaps,FinishedBy,Contains,Starts,Equals,StartedBy,During,Finishes,OverlappedBy,MetBy,After]
print empty
---> fromList []
Constructing custom composite predicates
The following examples demonstrate some approaches for constructing predicate functions.
-- We can construct a predicate function from a 'Set IntervalRelation'
endedPrior
:: (Ord a, Intervallic i0, Intervallic i1)
=> ComparativePredicateOf2 (i0 a) (i1 a)
endedPrior = predicate (fromList [Before, Meets])
-- We can also construct a predicate function directly from a list of predicate
-- functions
endedPrior'
:: (Ord a, Intervallic i0, Intervallic i1)
=> ComparativePredicateOf2 (i0 a) (i1 a)
endedPrior' = unionPredicates [before, meets]
-- As an alternative to `unionPredicates` we can compose predicate functions
-- using the <|> operator. If we had multiple predicates we could use e.g.:
-- p1 <|> p2 <|> p3
endedPrior''
:: (Ord a, Intervallic i0, Intervallic i1)
=> ComparativePredicateOf2 (i0 a) (i1 a)
endedPrior'' = before <|> meets
print $ iv0to2 `precedes` iv3to6
print $ iv0to2 `meets` iv3to6
print $ iv0to2 `endedPrior` iv3to6
---> True
---> False
---> True
print $ iv0to2 `precedes` iv2to4
print $ iv0to2 `meets` iv2to4
print $ iv0to2 `endedPrior` iv2to4
---> False
---> True
---> True
print $ iv5to8 `precedes` iv2to4
print $ iv5to8 `meets` iv2to4
print $ iv5to8 `endedPrior` iv2to4
---> False
---> False
---> False
An example interval-algebra application
Problem description
In this section we present an example application taken from the domain of epidemiological studies. For this example, we want to compare the difference, if any, between an industry standard vaccine and a new-to-market vaccine in preventing a person from contracting influenza (flu). The primary information that we wish to obtain is how long it takes people to contract flu after taking a vaccine; however we only observe a given person for a finite amount of time and they might not get a vaccine or they might not contract flu (or neither) during the time while they are under observation.
Input data
In order to study our research question, we collect data for multiple subjects. For a given subject, we can have data that includes the following types of health care events and subject metadata. Our interest is in a flu diagnosis.
-
Enrollment: a period in time when a subject was under observation for the study. Subjects may drop out of the study at any time, and may have gaps of time where they were not observed in the study. If there are gaps between enrollment periods, there is an allowed grace period during which if the subject re-enters observation then they will still be considered eligible for the study. If however, the grace period expires and the subject comes back under observation then any data after that will be considered inadmissible. For this example, we will consider the grace period to be 8 (see the note below about the unit of time).
-
Treatment: Either the standard vaccine or the alternative vaccine.
-
Diagnosis: One of several possible diagnoses.
For simplicity the Interval endpoints are provided as Integers (and hence the corresponding durations are also Integers),
however in a more realistic setting the endpoints would more likely be something like a Data.Time.Day.
Target processed data results
The information that we would like to extract from the input data is the following. We will see in the upcoming Data model section that additional information is included in the output data type, but this is only supplemental data included for the reader’s understanding.
-
The treatment type of the first vaccination treatment, if applicable.
-
The amount of time between the end of the treatment vaccination period and the start of the first flu diagnosis period, if applicable.
-
The amount of time between the end of the treatment vaccination period and the end of the overall eligibility period, if applicable.
Data model
The types that we will use to model our problem domain are the following:
-
StudyEventis the basic unit of observation. -
SubjEventsis a list ofStudyEvents that are assumed to all correspond to a given subject, and to be complete in the sense that there are no otherStudyEvents for that particular subject that are not included in the list. -
ProcessedSubjis the type of the target output information for a given subject.
data DataType = Enrollment | Treatment TreatmentType | Diagnosis DiagnosisType
deriving (Eq, Ord, Show)
data TreatmentType = StandardVaccine | NewVaccine
deriving (Eq, Ord, Show)
data DiagnosisType = RightAsRain | UpsetTummy | CommonCold | Flu
deriving (Eq, Ord, Show)
type StudyEvent = PairedInterval DataType Integer
type SubjEvents = [StudyEvent]
data ProcessedSubj = ProcessedSubj
{ getEnrollment :: Maybe (Interval Integer)
, getFirstTrt :: Maybe StudyEvent
, getFirstFlu :: Maybe StudyEvent
, getTrtType :: Maybe TreatmentType
, getTimeToFlu :: Maybe Integer
, getTimeToEndEnr :: Maybe Integer
}
deriving Show
maxEnrGap :: Integer
maxEnrGap = 8
Example data
Some example data is constructed below.
In all, there are four subjects, with data collected in the variables id1Events,
id2Events, id3Events, and id4Events.
To skip ahead and see the results produced by our data processing functions (introduced later) see the Calculate results section.
It is important to note that the data is sorted chronologically within a given subject; some of the routines that we will see later depend on this being the case.
Example data subject 1
-
For the "id1" subject we see an example where all of the enrollment periods fall within the allotted grace period of 8 days, so the overall study period has a starting point of 6 and an ending point of 430 (i.e. 8 days after the ending point of the last enrollment period).
-
The
Diagnosis RightAsRainandDiagnosis CommonColdentries are not relevant to the study definition and should effectively be ignored. -
The treatment type is
NewVaccinewith an ending point of 23, and we don’t see any flu occurrence. -
The time to the end of enrollment is the difference between the end of the first treatment period and the end of the overall study period, which for this example is 407 (i.e. 430 - 23).
id1Event1, id1Event2, id1Event3, id1Event4, id1Event5, id1Event6, id1Event7
:: StudyEvent
id1Event1 = makePairedInterval Enrollment (safeInterval (6, 191))
id1Event2 = makePairedInterval Enrollment (safeInterval (199, 345))
id1Event3 = makePairedInterval Enrollment (safeInterval (347, 422))
id1Event4 = makePairedInterval (Diagnosis RightAsRain) (safeInterval (12, 13))
id1Event5 = makePairedInterval (Treatment NewVaccine) (safeInterval (22, 23))
id1Event6 =
makePairedInterval (Diagnosis RightAsRain) (safeInterval (131, 132))
id1Event7 = makePairedInterval (Diagnosis CommonCold) (safeInterval (161, 162))
id1Events :: SubjEvents
id1Events = sort
[id1Event1, id1Event2, id1Event3, id1Event4, id1Event5, id1Event6, id1Event7]
Example data subject 2
-
For the "id2" subject we see an example where the start of the second enrollment period does not fall within the allotted grace period of 8 days after the first enrollment period, so the overall study period has a starting point of 2 and an ending point of 214 (i.e. 8 days after the ending point of the first enrollment period).
-
The
Diagnosis RightAsRain,Diagnosis CommonColdandDiagnosis UpsetTummyentries are not relevant to the study definition and should effectively be ignored. -
The treatment type is
StandardVaccinewith an ending point of 99, and although we do see a flu occurrence, it happens outside of the overall study period and should effectively be ignored. -
The time to the end of enrollment is the difference between the end of the first treatment period and the end of the overall study period, which for this example is 115 (i.e. 214 - 99).
id2Event1, id2Event2, id2Event3, id2Event4, id2Event5, id2Event6, id2Event7, id2Event8
:: StudyEvent
id2Event1 = makePairedInterval Enrollment (safeInterval (2, 206))
id2Event2 = makePairedInterval Enrollment (safeInterval (222, 299))
id2Event3 = makePairedInterval Enrollment (safeInterval (304, 486))
id2Event4 = makePairedInterval (Diagnosis RightAsRain) (safeInterval (4, 5))
id2Event5 =
makePairedInterval (Treatment StandardVaccine) (safeInterval (98, 99))
id2Event6 = makePairedInterval (Diagnosis CommonCold) (safeInterval (161, 162))
id2Event7 = makePairedInterval (Diagnosis UpsetTummy) (safeInterval (191, 192))
id2Event8 = makePairedInterval (Diagnosis Flu) (safeInterval (255, 256))
id2Events :: SubjEvents
id2Events = sort
[ id2Event1
, id2Event2
, id2Event3
, id2Event4
, id2Event5
, id2Event6
, id2Event7
, id2Event8
]
Example data subject 3
-
For the "id3" subject we see an example where we have just one enrollment period, so the overall study period has a starting point of 7 and an ending point of 205 (i.e. 8 days after the ending point of the enrollment period).
-
The treatment type is
StandardVaccinewith an ending point of 20, and we see a flu occurrence with a starting point of 180. Thus, the time to first (and only) flu diagnosis is 160 (i.e. 180 - 20) -
The time to the end of enrollment is the difference between the end of the first treatment period and the end of the overall study period, which for this example is 185 (i.e. 205 - 20).
id3Event1, id3Event2, id3Event3 :: StudyEvent
id3Event1 = makePairedInterval Enrollment (safeInterval (7, 197))
id3Event2 =
makePairedInterval (Treatment StandardVaccine) (safeInterval (19, 20))
id3Event3 = makePairedInterval (Diagnosis Flu) (safeInterval (180, 181))
id3Events :: SubjEvents
id3Events = sort [id3Event1, id3Event2, id3Event3]
Example data subject 4
-
For the "id4" subject we see an example where we have just one enrollment period, so the overall study period has a starting point of 3 and an ending point of 97 (i.e. 8 days after the ending point of the enrollment period).
-
The
Diagnosis RightAsRainentry is not relevant to the study definition and should effectively be ignored. -
Since there is no treatment event and no flu event there is no time to flu occurrence and no time to the end of enrollment.
id4Event1, id4Event2 :: StudyEvent
id4Event1 = makePairedInterval Enrollment (safeInterval (3, 89))
id4Event2 = makePairedInterval (Diagnosis RightAsRain) (safeInterval (47, 48))
id4Events :: SubjEvents
id4Events = sort [id4Event1, id4Event2]
Data processing functions
In this section we provide the data processing functions that we can use to calculate the desired output as described in the previous sections.
-- Construct the elements of a `ProcessedSubj` one step at-a-time. Most of the
-- actual work is done by the helper functions defined below
processSubj :: SubjEvents -> ProcessedSubj
processSubj xs =
let enrPeriod = calcEnrPeriod xs -- enrollment period
enrEvents = calcEnrEvents enrPeriod xs -- events within enrollment
firstTrt = findFirstTrt enrPeriod xs -- first treatment in enr
firstTrtType = extractTrtType firstTrt -- first trt type
firstTrtIv = fmap getInterval firstTrt -- first trt interval
firstFlu = findFirstFlu firstTrtIv enrEvents -- first flu in enr
ttFlu = calcDiff firstFlu firstTrt -- time to first flu
ttEndEnr = calcAtRisk enrPeriod firstTrt -- time to end of enr
in ProcessedSubj enrPeriod firstTrt firstFlu firstTrtType ttFlu ttEndEnr
-- Construct the "enrollment period", which is defined at the period of time
-- with the start endpoint given by their earliest enrollment period and end
-- endpoint given by the first time that they fall out of the grace period. In
-- the event that a subject did not have any enrollment periods then the return
-- value is a Nothing.
--
-- Note that this function uses the `combineIntervals` function which was not
-- covered in this tutorial, but is exported from IntervalAlgebra via
-- IntervalAlgebra.IntervalUtilities.
calcEnrPeriod :: SubjEvents -> Maybe (Interval Integer)
calcEnrPeriod xs | null combinedPeriods = Nothing
| otherwise = Just (head combinedPeriods)
where combinedPeriods = (combineIntervals . addMaxEnrGap . extractEnrIvs) xs
-- Filter the enrollment events in the SubjEvents and extract the Interval
-- from each one
extractEnrIvs :: SubjEvents -> [Interval Integer]
extractEnrIvs = intervals . filter (checkEnr . getPairData)
where
checkEnr Enrollment = True
checkEnr _ = False
-- Extend the end endpoint for each Interval by `maxEnrGap`
addMaxEnrGap :: [Interval Integer] -> [Interval Integer]
addMaxEnrGap = map (expandr maxEnrGap)
-- Filter the SubjEvents to those with endpoints that do not extend past the
-- enrollment period's endpoints
calcEnrEvents :: Maybe (Interval Integer) -> SubjEvents -> SubjEvents
calcEnrEvents mayIv xs = case mayIv of
Nothing -> []
Just y -> filter (\x -> getInterval x `enclosedBy` y) xs
-- Find the first flu vaccine administrations occuring within the enrollment
-- period
findFirstTrt :: Maybe (Interval Integer) -> SubjEvents -> Maybe StudyEvent
findFirstTrt Nothing _ = Nothing
findFirstTrt (Just iv) xs | null filteredIntakes = Nothing
| otherwise = Just (head filteredIntakes)
where
p x = checkTrt (getPairData x) && (getInterval x `enclosedBy` iv)
checkTrt (Treatment _) = True
checkTrt _ = False
filteredIntakes = filter p xs
-- Extract the treatment type out of a StudyEvent. If there is no event or the
-- StudyEvent type isn't TreatmentType then return Nothing
extractTrtType :: Maybe StudyEvent -> Maybe TreatmentType
extractTrtType Nothing = Nothing
extractTrtType (Just x) = case getPairData x of
Treatment StandardVaccine -> Just StandardVaccine
Treatment NewVaccine -> Just NewVaccine
_ -> Nothing
-- Find the first flu diagnosis occuring within the enrollment period
findFirstFlu :: Maybe (Interval Integer) -> SubjEvents -> Maybe StudyEvent
findFirstFlu Nothing _ = Nothing
findFirstFlu (Just iv) xs | null filteredFlus = Nothing
| otherwise = Just (head filteredFlus)
where
endedPrior = before <|> meets
p x = checkDiagFlu (getPairData x) && (iv `endedPrior` getInterval x)
checkDiagFlu (Diagnosis Flu) = True
checkDiagFlu _ = False
filteredFlus = filter p xs
-- Calculate the difference between the start endpoint of the first Intervallic
-- and the start endpoint of the second Intervallic
calcDiff
:: (IntervalSizeable a b, Intervallic i0, Intervallic i1)
=> Maybe (i0 a)
-> Maybe (i1 a)
-> Maybe b
calcDiff (Just y) (Just x) = Just $ diff (begin y) (end x)
calcDiff _ _ = Nothing
-- Calculate the difference between the end endpoint of the first Intervallic
-- and the start endpoint of the second Intervallic
calcAtRisk
:: (IntervalSizeable a b, Intervallic i0, Intervallic i1)
=> Maybe (i0 a)
-> Maybe (i1 a)
-> Maybe b
calcAtRisk (Just y) (Just x) = Just $ diff (end y) (end x)
calcAtRisk _ _ = Nothing
Calculate results
Here we calculate and print the results of the data processing.
results :: [ProcessedSubj]
results = map processSubj [id1Events, id2Events, id3Events, id4Events]
print $ head results
---> ProcessedSubj {getEnrollment = Just (6, 430), getFirstTrt = Just {(22, 23), Treatment NewVaccine}, getFirstFlu = Nothing, getTrtType = Just NewVaccine, getTimeToFlu = Nothing, getTimeToEndEnr = Just 407}
print $ results !! 1
---> ProcessedSubj {getEnrollment = Just (2, 214), getFirstTrt = Just {(98, 99), Treatment StandardVaccine}, getFirstFlu = Nothing, getTrtType = Just StandardVaccine, getTimeToFlu = Nothing, getTimeToEndEnr = Just 115}
print $ results !! 2
---> ProcessedSubj {getEnrollment = Just (7, 205), getFirstTrt = Just {(19, 20), Treatment StandardVaccine}, getFirstFlu = Just {(180, 181), Diagnosis Flu}, getTrtType = Just StandardVaccine, getTimeToFlu = Just 160, getTimeToEndEnr = Just 185}
print $ results !! 3
---> ProcessedSubj {getEnrollment = Just (3, 97), getFirstTrt = Nothing, getFirstFlu = Nothing, getTrtType = Nothing, getTimeToFlu = Nothing, getTimeToEndEnr = Nothing}