Outatime Build status NuGet Status

Outatime

Intro

When you have a lots of values changing over the time in a planning, it become difficult to deal with it day per day. The solution is to group the same values and merge period. On way is having the temporary (type containing value on a period) everywhere but: you have the domain complexity everywhere and if you have to compose another domain you will have more wrapped types.

To simplify and compose functionalities, Outatime give an applicative functor approach. Instead of having the wrapped type everywhere, a function containing all values as parameter is binded to values changing over the time. The domain is out of the time

How it works

By using merge function, intersection period, and finding the largest period for missing values, a function is applied each time you give the temporaries. Each time the value is the function applied (given a function with n parameter, expect a function with n-1 parameter).

Maths at Rescue! What are the common value over the time to avoid data repetition

Outatime has differents principles in order to have the correct design and algorithms

Period

Based on Mathematics Interval applied to Date with a Start Date and an End Date. The Interval is Half Open, then the period from 01/01 to 02/01 has one day duration.

Temporary

Is the value at a Period

Merge

When value are equals and period intersects, then the temporary is merged and number of temporaries on a temporal decrease. Eg Given a value "toto" on Given periods 01/01 -> 02/01; 02/01 -> 03/01, then the corresponding merge is "toto" on Given periods 01/01 -> 03/01. Sample : https://github.com/cboudereau/Outatime/blob/master/src/Outatime.Test/MergeTests.fs

open Outatime
open Bdd
open Xunit

let jan15 n = (DateTime(2015,1,n))

let ``I want to merge temporaries`` v = v |> Outatime.build |> Outatime.merge |> Outatime.toList

[<Fact>]
let ``given contiguous temporary expect a merged temporary``()=
    When ``I want to merge temporaries`` 
    |> With 
        [ jan15 01 => jan15 02 := "Hello"
          jan15 02 => jan15 05 := "Hello"
          jan15 05 => jan15 10 := "World"
          jan15 10 => jan15 20 := "World" ]
    |> Expect
        [ jan15 01 => jan15 05 := "Hello"
          jan15 05 => jan15 20 := "World" ]

Split

When need to crop to a period..

open Outatime
open Xunit
open Bdd

let jan15 n = (DateTime(2015,1,n))
let days n = TimeSpan.FromDays(float n)

let ``I want to split temporaries`` days temporaries = temporaries |> Outatime.build |> Outatime.split days |> Outatime.toList
let ``five days`` = days 5

[<Fact>]
let ``given temporaries for a large period when split for n days expect temporary with n day max period length``()=
    When ``I want to split temporaries`` 
    |> For ``five days``
    |> With [ jan15 01 => jan15 11 := "HelloWorld" ]
    |> Expect
        [ jan15 01 => jan15 06 := "HelloWorld"
          jan15 06 => jan15 11 := "HelloWorld" ]

Clamp

Given a period and get the corresponding temporaries.

QA

Writen with property based testing driven dev

Outatime is partially written with property based testing and FSCheck https://github.com/fscheck/FsCheck

Applicative functor

As describe in the introduction part, an applicative functor can be use in order to have a domain out of the time.

module ReadmeSample

open Outatime
open Xunit

let (<!>) f x = x |> Outatime.build |> Outatime.contiguous |> Outatime.map f
let (<*>) f x = x |> Outatime.build |> Outatime.contiguous |> Outatime.apply f

//Simple BDD functions
let When f = f
let With v f = f v
let For = With
let And = With
let Then check expected = check expected

let shouldEqual<'a> (expected:'a) (actual:'a) = Assert.Equal(expected, actual)

let Expect<'a> = Then shouldEqual<'a>

let shouldBeEmpty actual = Assert.Equal(0, actual |> Seq.length)

let jan15 d = DateTime(2015, 1, d)

//A domain sample
type Opening = Opened | Closed
type Departure = OpenedToDeparture | ClosedToDeparture
type Availability = Availability of int
type Price = Price of decimal

type Rate = 
    { Departure: Departure
      Availability: Availability
      Price: Price }

type RateAvailability = 
    | Closed
    | Opened of Rate

let ``transform temporaries to rate availability domain`` _ openingO departureO availabilityO priceO = 
    match openingO, departureO, availabilityO, priceO with
    | Some opening, Some departure, Some availability, Some price -> 
        match opening with
        | Opening.Opened ->
            RateAvailability.Opened 
                { Departure=departure; Availability=availability; Price=price }
            |> Some
        | Opening.Closed -> Some RateAvailability.Closed
    | _ -> None

let ``transform temporaries into request`` temporal = 
    let request t = 
            let toString (date:DateTime) = date.ToString("yyyy/MM/dd")
            let start = t |> period |> start |> toString
            let enD = t |> period |> enD |> toString

            match t.Value with
            | Closed -> sprintf "[%s; %s[ = Closed" start enD
            | Opened rate -> 
                let (Availability a) = rate.Availability
                let (Price p) = rate.Price
                let d = 
                    match rate.Departure with
                    | ClosedToDeparture -> "closed to departure"
                    | OpenedToDeparture -> "opened to departure"

                sprintf "[%s; %s[ = Opened with %i of availibility at %.2f price and %s" start enD a p d

    temporal
    |> Outatime.merge
    |> Outatime.ofOption
    |> Outatime.toList 
    |> List.map request

[<Fact>]
let ``given temporaries with no intersections or empty periods expect the largest period with none value``()=
    When
        ``transform temporaries to rate availability domain``
        <!> [ jan15 4  => jan15 4  := Opening.Opened
              jan15 5  => jan15 5 := Opening.Closed ]

        <*> [ jan15 1 => jan15 16 := OpenedToDeparture
              jan15 2  => jan15 2 := OpenedToDeparture ]

        <*> [ jan15 1  => jan15 1 := Availability 10 ]

        <*> [ jan15 3  => jan15 3 := Price 120m ]
        |> ``transform temporaries into request``
    |> Expect [ ]


[<Fact>]
let ``given multiple temporaries, when apply a function on this temporaries then expect applied function on any intersection``()=

    When
        ``transform temporaries to rate availability domain``
        <!> [ jan15 4  => jan15 5  := Opening.Opened
              jan15 5  => jan15 20 := Opening.Closed ]

        <*> [ jan15 2  => jan15 15 := OpenedToDeparture
              jan15 16 => jan15 18 := OpenedToDeparture
              jan15 18 => jan15 23 := ClosedToDeparture ]

        <*> [ jan15 1  => jan15 22 := Availability 10 ]

        <*> [ jan15 1  => jan15 22 := Price 120m ]
        |> ``transform temporaries into request``
    |> Expect 
        [ "[2015/01/04; 2015/01/05[ = Opened with 10 of availibility at 120.00 price and opened to departure"
          "[2015/01/05; 2015/01/15[ = Closed"
          "[2015/01/16; 2015/01/20[ = Closed" ]