ianmackenzie / elm-units-interval / Quantity.Interval

This modules contains most of the core functionality for creating and working with Interval values.

Note: most examples assume that you have imported this module as

import Quantity.Interval as Interval


type Interval number units

Represents a finite, closed interval with a minimum and maximum value.

The two type parameters match those of the Quantity type from elm-units. For example, an Interval Int Pixels might represent a value between 200 and 300 pixels, and an Interval Float Meters might represent a value between 2.5 and 3.7 centimeters. (As with the Quantity type, the units type parameter refers to the base unit of a particular quantity type; lengths in meters, centimeters, feet, miles etc. are all internally stored in meters.)

Constructors

fromEndpoints : ( Quantity number units, Quantity number units ) -> Interval number units

Construct an interval from its endpoints (the minimum and maximum values of the interval).

highwayCarSpeeds =
    Interval.fromEndpoints
        ( Speed.kilometersPerHour 90
        , Speed.kilometersPerHour 130
        )

The two values should be given in order but will be swapped if necessary to ensure a valid interval is returned:

Interval.endpoints <|
    Interval.fromEndpoints
        ( Speed.kilometersPerHour 130
        , Speed.kilometersPerHour 90
        )
--> ( Speed.kilometersPerHour 90
--> , Speed.kilometersPerHour 130
--> )

singleton : Quantity number units -> Interval number units

Construct a zero-width interval containing a single value.

Interval.singleton (Length.meters 3)
--> Interval.fromEndpoints
-->     ( Length.meters 3
-->     , Length.meters 3
-->     )

Hull

These functions let you construct an Interval containing one or more input values.

hull2 : Quantity number units -> Quantity number units -> Interval number units

Construct an interval containing the two given values (which can be provided in either order). hull2 a b is equivalent to fromEndpoints ( a, b ).

-- "The heights of people participating in the study
-- ranged from 1.2 to 1.9 meters"
heightRange =
    Interval.hull2
        (Length.meters 1.2)
        (Length.meters 1.9)

-- "Please allow 4 to 6 weeks for delivery"
estimatedShippingTime =
    Interval.hull2
        (Duration.weeks 4)
        (Duration.weeks 6)

hull3 : Quantity number units -> Quantity number units -> Quantity number units -> Interval number units

Construct an interval containing the three given values;

Interval.hull3 a b c

is equivalent to

Interval.hull a [ b, c ]

but is more efficient.

hull4 : Quantity number units -> Quantity number units -> Quantity number units -> Quantity number units -> Interval number units

Construct an interval containing the four given values.

hull : Quantity number units -> List (Quantity number units) -> Interval number units

Find the interval containing one or more input values, by passing the first value as the first argument and then a list of all other values as the second argument. For example, to find the interval containing the values 5 cm, 2 cm, 3 cm and 4 cm:

Interval.hull
    (Length.centimeters 5)
    [ Length.centimeters 3
    , Length.centimeters 2
    , Length.centimeters 4
    ]
--> Interval.fromEndpoints
-->     ( Length.centimeters 2
-->     , Length.centimeters 5
-->     )

Why pass the first and all other values as separate arguments? It lets this function return an Interval instead of a Maybe Interval. If there was just a single list as an argument, then this function would have to handle the case of an empty list being passed by returning Nothing. As a result, when using this function you often end up using it within a case expression:

case values of
    [] ->
        -- some default behavior

    first :: rest ->
        let
            interval =
                Interval.hull first rest
        in
        -- normal behavior using 'interval'

If you do want the simpler behavior of taking a single list and returning a Maybe Interval, check out hullN.

hullN : List (Quantity number units) -> Maybe (Interval number units)

Attempt to construct an interval containing all N values in the given list. If the list is empty, returns Nothing. If you know you have at least one value, you can use hull instead.

Interval.hullN
    [ Duration.hours 2
    , Duration.hours 1
    , Duration.hours 3
    ]
--> Just <|
-->     Interval.fromEndpoints
-->         ( Duration.hours 1
-->         , Duration.hours 3
-->         )

Interval.hullN [ Duration.hours 5]
--> Just (Interval.singleton (Duration.hours 5))

Interval.hullN []
--> Nothing

hullOf : (a -> Quantity number units) -> a -> List a -> Interval number units

Like hull, but lets you work on any kind of item as long as a Quantity of some kind can be extracted from it. For example, if you had

type alias Person =
    { name : String
    , age : Duration
    }

then given some people you could find their range of ages as an Interval Float Seconds using

Interval.hullOf .age
    firstPerson
    [ secondPerson
    , thirdPerson
    , fourthPerson
    ]

See also hullOfN.

hullOfN : (a -> Quantity number units) -> List a -> Maybe (Interval number units)

Combination of hullOf and hullN.

Aggregation

These functions let you 'aggregate' one or more intervals into a single larger interval that contains all of them.

aggregate2 : Interval number units -> Interval number units -> Interval number units

Construct an interval containing both of the given intervals.

firstInterval =
    Interval.fromEndpoints
        ( Length.feet 1, Length.feet 2 )

secondInterval =
    Interval.fromEndpoints
        ( Length.feet 3, Length.feet 6 )

Interval.aggregate2 firstInterval secondInterval
--> Interval.fromEndpoints
-->     ( Length.feet 1, Length.feet 6 )

aggregate3 : Interval number units -> Interval number units -> Interval number units -> Interval number units

Construct an interval containing all three of the given intervals;

Interval.aggregate3 first second third

is equivalent to

Interval.aggregate first [ second, third ]

but is more efficient.

aggregate4 : Interval number units -> Interval number units -> Interval number units -> Interval number units -> Interval number units

Construct an interval containing all four of the given intervals.

aggregate : Interval number units -> List (Interval number units) -> Interval number units

Construct an interval containing one or more given intervals:

Interval.aggregate
    (Interval.singleton (Length.feet 2))
    [ Interval.fromEndpoints
        ( Length.feet 3
        , Length.feet 4
        )
    ]
--> Interval.fromEndpoints
-->     ( Length.feet 2, Length.feet 4 )

Works much like hull. See also aggregateN.

aggregateN : List (Interval number units) -> Maybe (Interval number units)

Attemp to construct an interval containing all of the intervals in the given list. If the list is empty, returns Nothing. If you know you have at least one interval, you can use aggregate instead.

aggregateOf : (a -> Interval number units) -> a -> List a -> Interval number units

Like aggregate, but lets you work on any kind of item as long as an interval can be generated from it (similar to hullOf).

aggregateOfN : (a -> Interval number units) -> List a -> Maybe (Interval number units)

Combination of aggregateOf and aggregateN.

Properties

endpoints : Interval number units -> ( Quantity number units, Quantity number units )

Get the endpoints of an interval (its minimum and maximum values) as a tuple. The first value will always be less than or equal to the second.

( minValue, maxValue ) =
    Interval.endpoints someInterval

For any interval,

Interval.endpoints interval

is equivalent to (but more efficient than)

( Interval.minValue interval
, Interval.maxValue interval
)

minValue : Interval number units -> Quantity number units

Get the minimum value of an interval.

maxValue : Interval number units -> Quantity number units

Get the maximum value of an interval.

midpoint : Interval Basics.Float units -> Quantity Basics.Float units

Get the midpoint of an interval.

Interval.midpoint <|
    Interval.fromEndpoints
        ( Duration.hours 2
        , Duration.hours 3
        )
--> Duration.hours 2.5

width : Interval number units -> Quantity number units

Get the width of an interval.

Interval.width <|
    Interval.fromEndpoints
        ( Length.meters 1.2
        , Length.meters 1.35
        )
--> Length.centimeters 15

Queries

contains : Quantity number units -> Interval number units -> Basics.Bool

Check if an interval contains a given value:

angleInterval =
    Interval.fromEndpoints
        ( Angle.degrees -10
        , Angle.degrees 30
        )

angleInterval |> Interval.contains (Angle.degrees 0)
--> True

angleInterval |> Interval.contains (Angle.degrees 45)
--> False

The minimum and maximum values of an interval are considered to be contained in the interval (but be careful of numerical roundoff):

angleInterval |> Interval.contains (Angle.degrees 30)
--> True

isContainedIn : Interval number units -> Interval number units -> Basics.Bool

Check if the second interval is fully contained in the first.

angleInterval =
    Interval.fromEndpoints
        ( Angle.degrees -30, Angle.degrees 30 )

Interval.fromEndpoints
    ( Angle.degrees -5, Angle.degrees 15 )
    |> Interval.isContainedIn angleInterval
--> True

Interval.fromEndpoints
    ( Angle.degrees 15, Angle.degrees 45 )
    |> Interval.isContainedIn angleInterval
--> False

Be careful with the argument order! If not using the |> operator, the first example would be written as:

Interval.isContainedIn angleInterval
    (Interval.fromEndpoints
        ( Angle.degrees -5
        , Angle.degrees 15
        )
    )
--> True

intersects : Interval number units -> Interval number units -> Basics.Bool

Check if two intervals touch or overlap (have any values in common).

distanceInterval =
    Interval.fromEndpoints
        ( Length.kilometers 5
        , Length.kilometers 10
        )

distanceInterval
    |> Interval.intersects
        (Interval.fromEndpoints
            ( Length.kilometers 8
            , Length.kilometers 12
            )
        )
--> True

distanceInterval
    |> Interval.intersects
        (Interval.fromEndpoints
            ( Length.kilometers 12
            , Length.kilometers 15
            )
        )
--> False

Intervals that just touch each other are considered to intersect (this is consistent with intersection which will return a zero-width interval for the intersection of two just-touching intervals):

distanceInterval
    |> Interval.intersects
        (Interval.fromEndpoints
            ( Length.kilometers 10
            , Length.kilometers 15
            )
        )
--> True

intersection : Interval number units -> Interval number units -> Maybe (Interval number units)

Attempt to construct an interval containing all the values common to both given intervals:

Interval.intersection
    (Interval.fromEndpoints
        ( Mass.grams 1, Mass.grams 3 )
    )
    (Interval.fromEndpoints
        ( Mass.grams 2, Mass.grams 5 )
    )
--> Just <|
-->     Interval.fromEndpoints
-->         ( Mass.grams 2, Mass.grams 3 )

If the intervals do not intersect, returns Nothing:

Interval.intersection
    (Interval.fromEndpoints
        ( Mass.grams 1, Mass.grams 3 )
    )
    (Interval.fromEndpoints
        ( Mass.grams 4, Mass.grams 7 )
    )
--> Nothing

If the two intervals just touch, a singleton interval will be returned:

Interval.intersection
    (Interval.fromEndpoints
        ( Mass.grams 1, Mass.grams 3 )
    )
    (Interval.fromEndpoints
        ( Mass.grams 3, Mass.grams 5 )
    )
--> Just (Interval.singleton (Mass.grams 3))

isSingleton : Interval number units -> Basics.Bool

Check if the interval is a singleton (the minimum and maximum values are the same).

Interval.isSingleton <|
    Interval.fromEndpoints
        ( Length.meters 2, Length.meters 2 )
--> True

Interval.isSingleton <|
    Interval.fromEndpoints
        ( Length.meters 2, Length.meters 3 )
--> False

Interpolation

interpolate : Interval Basics.Float units -> Basics.Float -> Quantity Basics.Float units

Interpolate between an interval's endpoints based on a parameter value. A value of 0.0 corresponds to the minimum value of the interval, a value of 0.5 corresponds to its midpoint and a value of 1.0 corresponds to its maximum value:

lengthInterval =
    Interval.fromEndpoints
        ( Length.meters 1, Length.meters 5 )

Interval.interpolate lengthInterval 0
--> Length.meters 1

Interval.interpolate lengthInterval 0.75
--> Length.meters 4

Values less than 0.0 or greater than 1.0 can be used to extrapolate:

Interval.interpolate lengthInterval 1.5
--> Length.meters 7

Note that because of how Interval.fromEndpoints works, the interpolation is in fact from the minimum value to the maximum, not "from the first argument to the second":

Interval.interpolate
    (Interval.fromEndpoints
        ( Length.meters 0
        , Length.meters 10
        )
    )
    0.2
--> 2

Interval.interpolate
    (Interval.fromEndpoints
        ( Length.meters 10
        , Length.meters 0
        )
    )
    0.2
--> 2 -- not 8!

If you want to interpolate from one number down to another, you can use Quantity.interpolateFrom from the elm-units package.

interpolationParameter : Interval Basics.Float units -> Quantity Basics.Float units -> Basics.Float

Given an interval and a given value, determine the corresponding interpolation parameter (the parameter that you would pass to interpolate to get the given value):

Interval.interpolationParameter
    (Interval.fromEndpoints
        ( Duration.minutes 10
        , Duration.minutes 15
        )
    )
    (Duration.minutes 12)
--> 0.4

The result will be between 0 and 1 if (and only if) the given value is contained in the given interval:

Interval.interpolationParameter
    (Interval.fromEndpoints
        ( Duration.minutes 10
        , Duration.minutes 15
        )
    )
    (Duration.minutes 18)
--> 1.6

Interval.interpolationParameter
    (Interval.fromEndpoints
        ( Duration.minutes 10
        , Duration.minutes 15
        )
    )
    (Duration.minutes 9)
--> -0.2

This is the inverse of interpolate; for any non-zero-width interval,

Interval.interpolationParameter interval value
    |> Interval.interpolate interval

should be equal to the original value (within numerical roundoff).

Arithmetic

These functions let you do math with Interval values, following the rules of interval arithmetic.

negate : Interval number units -> Interval number units

Negate an interval. Note that this will flip the order of the endpoints.

Interval.negate <|
    Interval.fromEndpoints
        ( Angle.degrees 20
        , Angle.degrees 30
        )
--> Interval.fromEndpoints
-->     ( Angle.degrees -30
-->     , Angle.degrees -20
-->     )

multiplyBy : number -> Interval number units -> Interval number units

Multiply an interval by a given value:

Interval.multiplyBy 5 <|
    Interval.fromEndpoints
        ( Duration.minutes 20
        , Duration.minutes 30
        )
--> Interval.fromEndpoints
-->     ( Duration.minutes 100
-->     , Duration.minutes 150
-->     )

Note that this will flip the order of the interval's endpoints if the given value is negative:

Interval.multiplyBy -2 <|
    Interval.fromEndpoints
        ( Angle.degrees 20
        , Angle.degrees 30
        )
--> Interval.fromEndpoints
-->     ( Angle.degrees -60
-->     , Angle.degrees -40
-->     )

divideBy : Basics.Float -> Interval Basics.Float units -> Interval Basics.Float units

Divide an interval by a given value:

Interval.divideBy 2 <|
    Interval.fromEndpoints
        ( Length.feet 2, Length.feet 3 )
--> Interval.fromEndpoints
-->     ( Length.feet 1, Length.feet 1.5 )

Note that this will flip the order of the interval's endpoints if the given value is negative:

Interval.divideBy -2 <|
    Interval.fromEndpoints
        ( Angle.degrees 20
        , Angle.degrees 30
        )
--> Interval.fromEndpoints
-->     ( Angle.degrees -15
-->     , Angle.degrees -10
-->     )

half : Interval Basics.Float units -> Interval Basics.Float units

Shorthand for multiplyBy 0.5.

twice : Interval number units -> Interval number units

Shorthand for multiplyBy 2.

plus : Quantity number units -> Interval number units -> Interval number units

Add the given amount to an interval:

lengthInterval =
    Interval.fromEndpoints
        ( Length.meters 2, Length.meters 3 )

lengthInterval |> Interval.plus (Length.centinmeters 20)
--> Interval.fromEndpoints
-->     ( Length.meters 2.2, Length.meters 3.2 )

plusInterval : Interval number units -> Interval number units -> Interval number units

Add two intervals together.

firstInterval =
    Interval.fromEndpoints
        ( Length.feet 5, Length.feet 10 )

secondInterval =
    Interval.fromEndpoints
        ( Length.feet 2, Length.feet 3 )

firstInterval |> Interval.plus secondInterval
--> Interval.fromEndpoints
-->     ( Length.feet 7, Length.feet 13 )

minus : Quantity number units -> Interval number units -> Interval number units

Subtract the given amount from an interval.

angleInterval =
    Interval.fromEndpoints
        ( Angle.degrees -10
        , Angle.degrees 50
        )

angleInterval |> Interval.minus (Angle.degrees 30)
--> Interval.fromEndpoints
-->     ( Angle.degrees -40
-->     , Angle.degrees 20
-->     )

difference : Quantity number units -> Interval number units -> Interval number units

Subtract an interval from the given amount. So if you wanted to compute interval - quantity you would write

interval |> Interval.minus quantity

but if you wanted to compute quantity - interval then you would write

Interval.difference quantity interval

minusInterval : Interval number units -> Interval number units -> Interval number units

Subtract the first interval from the second. This means that minusInterval makes the most sense when using |>:

firstInterval =
    Interval.fromEndpoints
        ( Length.feet 5, Length.feet 10 )

secondInterval =
    Interval.fromEndpoints
        ( Length.feet 2, Length.feet 3 )

firstInterval |> Interval.minusInterval secondInterval
--> Interval.fromEndpoints
-->     ( Length.feet 2, Length.feet 8 )

Without the pipe operator, the above would be written as:

Interval.minusInterval secondInterval firstInterval

times : Quantity number quantityUnits -> Interval number intervalUnits -> Interval number (Quantity.Product intervalUnits quantityUnits)

Multiply an Interval by a Quantity, for example

interval |> Interval.times quantity

product : Quantity number quantityUnits -> Interval number intervalUnits -> Interval number (Quantity.Product quantityUnits intervalUnits)

Multiply an Interval by a Quantity, for example

Interval.product quantity interval

Note that unlike times, the units of the result will be Product quantityUnits intervalUnits, not Product intervalUnits quantityUnits.

timesUnitless : Quantity number Quantity.Unitless -> Interval number units -> Interval number units

Multiply an Interval by a unitless Quantity. See the documentation for Quantity.timesUnitless for more details.

timesInterval : Interval number units2 -> Interval number units1 -> Interval number (Quantity.Product units1 units2)

Multiply the second interval by the first. The order only matters if the two intervals have different units (since it will affect the units of the result) but, like other functions in this module, times is generally designed to be used with |>:

width =
    Interval.fromEndpoints
        ( Length.centimeters 10, Length.centimeters 12 )

height =
    Interval.fromEndpoints
        ( Length.centimeters 5, Length.centimeters 6 )

width |> Interval.times height
--> Interval.fromEndpoints
-->     ( Area.squareCentimeters 50
-->     , Area.squareCentimeters 72
-->     )

timesUnitlessInterval : Interval number Quantity.Unitless -> Interval number units -> Interval number units

Combination of timesInterval and timesUnitless for when one of the intervals in a product is unitless.

reciprocal : Interval Basics.Float Quantity.Unitless -> Interval Basics.Float Quantity.Unitless

Find the inverse of a unitless interval:

Interval.reciprocal <|
    Interval.fromEndpoints
        ( Quantity.float 2
        , Quantity.float 3
        )
--> Interval.fromEndpoints
-->     ( Quantity.float 0.333
-->     , Quantity.flaot 0.500
-->     )

Avoid using this function whenever possible, since it's very easy to get infinite intervals as a result:

Interval.reciprocal <|
    Interval.fromEndpoints
        ( Quantity.float -1
        , Quantity.float 2
        )
--> Interval.fromEndpoints
-->     ( Quantity.negativeInfinity
-->     , Quantity.negativeInfinity
-->     )

Since zero is contained in the above interval, the range of possible reciprocals ranges from negative to positive infinity!

abs : Interval number units -> Interval number units

Get the absolute value of an interval.

Interval.abs <|
    Interval.fromEndpoints
        ( Length.meters -3  Length.meters 2 )
--> Interval.fromEndpoints
-->     (Length.meters 0) (Length.meters 3)

squared : Interval number units -> Interval number (Quantity.Squared units)

Get the square of an interval.

squaredUnitless : Interval number Quantity.Unitless -> Interval number Quantity.Unitless

Specialized version of squared for unitless intervals.

cubed : Interval number units -> Interval number (Quantity.Cubed units)

Get the cube of an interval.

cubedUnitless : Interval number Quantity.Unitless -> Interval number Quantity.Unitless

Specialized version of cubed for unitless intervals.

Random value generation

randomValue : Interval Basics.Float units -> Random.Generator (Quantity Basics.Float units)

Create a random generator for quantities within a given interval. For example,

Interval.randomValue <|
    Interval.fromEndpoints
        ( Length.meters 5, Length.meters 10 )

is a Generator that will produce random lengths between 5 and 10 meters.

Deprecated

These functions are currently deprecated and will be removed in the next major release.

from : Quantity number units -> Quantity number units -> Interval number units

Deprecated alias for hull2.

union : Interval number units -> Interval number units -> Interval number units

Deprecated alias for aggregate2.