hedgehogqa / haskell-hedgehog

Release with confidence, state-of-the-art property testing for Haskell.
676 stars 107 forks source link

Add `UTCTime` generator #215

Open vrom911 opened 6 years ago

vrom911 commented 6 years ago

I know that this could be very difficult to implement, but it's very handy to have one.

Currently, I use very simple implementation like this:

genUTCTime :: MonadGen m => m UTCTime
genUTCTime = do
    y <- toInteger <$> Gen.int (Range.constant 2000 2019)
    m <- Gen.int (Range.constant 1 12)
    d <- Gen.int (Range.constant 1 28)
    let day = fromGregorian y m d
    secs <- toInteger <$> Gen.int (Range.constant 0 86401)
    let diff = secondsToDiffTime secs
    pure $ UTCTime day diff
moodmosaic commented 6 years ago

Yeah, we've been going back and forth on this for a while in the F# version and ended up generating a 64-bit integer in the form of a 64-bit FILETIME structure, called tick:

A date and time expressed in the number of 100-nanosecond intervals that have elapsed since January 1, 0001 at 00:00:00.000 in the Gregorian calendar.

let dateTime : Gen<System.DateTime> =
    let minTicks =
        System.DateTime.MinValue.Ticks
    let maxTicks =
        System.DateTime.MaxValue.Ticks
    gen {
        let! ticks =
            Range.constantFrom
                (System.DateTime (2000, 1, 1)).Ticks minTicks maxTicks
            |> integral
        return System.DateTime ticks
    }

(The above generator shrinks towards year 2000, and is responsible for generating the instants in time. This way, I hope, you don't have to worry about getting the number of days per month correct.)

Now, under UNIX platforms, file times are maintained in the form of a ANSI C runtime arithmetic type named time_t1, which represents seconds since midnight January 1, 1970 UTC (coordinated universal time).

Perhaps we can do similar here using EpochTime, convert to POSIXTime and then UTCTime, something I've never tried actually. However, the equivalent in F# works pretty well (so far).

--

1. How To Convert a UNIX time_t to a Win32 FILETIME or SYSTEMTIME

sevanspowell commented 5 years ago

@vrom911 Thanks, I'm giving this generator a go. I think there is one issue:

The seconds generated atm is [0, 86401] (where '[' is inclusive) but the UTC diffTime requires seconds in the range [0, 86401) (where ')' is exclusive): "the time from midnight, 0 <= t < 86401s (because of leap-seconds)".

So I think that line needs to be updated to:

    secs <- toInteger <$> Gen.int (Range.constant 0 86400)

Thanks again.

jacobstanley commented 5 years ago

The main concern I have with a time generator, isn't so much the implementation, but that there isn't really an agreed time data type in Haskell.

There are at lease three choices that I'm aware of when it comes to time packages (time, thyme and chronos) and it's not obvious to me which one to support, or if we should support them all. If the choice was obvious I would have had it from the start.

They all have pros and cons, with the vulnerable time library probably being the worst choice for production code given it uses Integer to represent everything! In the past I have defaulted to thyme, but it has a pretty heavy dependency stack. This is much better since they dropped lens as a required dependency, but it's pretty wild that it has a dependency on QuickCheck!

I haven't tried chronos but looks like it might be a happy medium, I especially like that it has nanosecond resolution while using a single Int64 instead of a big Integer. thyme also uses a single Int64 but only has microsecond resolution, so it's kind of useless for a whole class of applications (e.g. HFT or other high performance networking, etc)

It is tempting to include UTCTime from the time package on the basis that it comes with GHC out of the box. I just wouldn't want to encourage people to actually use it as it's usually the wrong choice.

jacobstanley commented 5 years ago

Fwiw, I would generate a posix time and convert it to UTCTime so you don't need to worry about seconds and months, etc. I think something like what F# is doing is the right approach.

sevanspowell commented 5 years ago

@jystic Thanks for shedding some light on that situation, I'm not super familiar with the time libraries in Haskell. I might give the POSIX time a go.

jacobstanley commented 5 years ago

It's such a nightmare, hard to believe that of all platforms (at least that I'm familiar with), the one from Microsoft probably has the best built in dates / times. The only thing I would improve about the .NET DateTime (assuming we don't worry about timezones) is nanosecond resolution. Also some of their conversion functions truncate to milliseconds but I guess that's the same issue.

jacobstanley commented 5 years ago

I think maybe a good way for us to solve this is to have a few small packages hedgehog-time, hedgehog-thyme, hedgehog-chrono, etc.

moodmosaic commented 5 years ago

have a few small packages hedgehog-time, hedgehog-thyme, hedgehog-chrono, etc.

This looks like a good idea :+1: Those packages should be so small and focused that could even go under https://github.com/hedgehogqa.

jacobstanley commented 5 years ago

Yeah I would be happy to have them in the haskell-hedgehog repo

domenkozar commented 3 years ago

Here are the imports for the given example in issue description:

import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Data.Time (UTCTime(..), fromGregorian, secondsToDiffTime)

As for merging this function, what's the harm to have it in hedgehog package given that time is part of GHC and no extra dependencies are needed?