Question

Consider a DateTime type where the date must be present, but the time part in seconds is optional. If the time part is there, there might be an optional milliseconds part, too. If milliseconds are present, there might be a nanoseconds part, too.

There are many ways to deal with this, e.g.:

--rely on smart constructors 
data DateTime = DateTime { days:: Int, 
                           sec :: Maybe Int, 
                           ms :: Maybe Int, 
                           ns :: Maybe Int 
                         }

-- list all possibilities
data DateTime = DateOnly Int 
              | DateWithSec Int Int
              | DateWithMilliSec Int Int Int
              | DateWithNanoSec Int Int Int Int    

-- cascaded Maybe
data DateTime = DateTime Int (Maybe (Int, Maybe (Int, Maybe Int)))

-- cascaded data
data Nano = NoNano | Nano Int
data MilliSec = NoMilliSec | MilliSec Int Nano
data Sec = NoSec | Sec Int MilliSec
data Date = Date Int Sec

Which construct would you use (of course not limited to the examples above), and why?

[Intentions]

I'm exploring the possibilities for a date type in Frege ( http://code.google.com/p/frege/ ), using date4j's DateTime as a guide line (as Haskell's date and time lib is way too complicated, and java.util.Date too broken). In my current toy implementation all fields are mandatory, but of course it would be nice to free the user from unwanted precision (and the original implementation has optional fields).

So the main goals are:

  • safety: Illegal states must be avoided at all costs
  • convenience: It should be easy to work with the type, e.g. pattern matching would be cool, calendar calculations should be easy...

Not so important are:

  • performance: Of course working with the type shouldn't be too slow, but for the typical usage it doesn't have to sqeeze out the last clock cycle
  • memory: In cases where this really matters, it would be easy to derive a more compact storage format
  • terse implementation: It's a library, and I'm willing to add all the code needed to make things smooth

That said, all of this is very tentative and shouldn't be taken too serious.

Was it helpful?

Solution

(This isn't an answer, but it's too long for a comment and will be clearer here.)

There is another way that you could handle it: have a single DateTime type that stores all fields always along with a parameter representing the precision, e.g.

data Precision = Days | Seconds | Milliseconds | Nanoseconds deriving (Ord, Eq {- etc -})
data DateTime = DateTime { prec :: Precision,
                           days :: Int, 
                           sec :: Int,
                           ms :: Int,
                           ns :: Int }

And use smart constructors that set the unused parameters to 0. If you have dateDifference or whatever, you can propagate the precision through (the Ord instance would make this neat).

(I've got little idea about how good/Haskell-y this is, but the other solutions seem quite messy, maybe this is more elegant.)

OTHER TIPS

“Illegal states must be avoided at all costs” and “pattern matching would be cool” are fine principles that in this case are in direct conflict with each other.

In addition, dates and times are gnarly human cultural constructs with lots of edge cases and irregular corners. They are not the sort of rules we can easily encode in the type system.

So in this case I would go with an opaque data type, smart constructors, and smart deconstructors. There's always view patterns and pattern guards for occasions when we want to use pattern matching.

(And I haven't even discussed dependent optional data as a motivating factor.)

Inspired by @dbaupp's solution I'd like to add a phantom type version to the candidates:

-- using EmptyDataDecls
data DayPrec 
data SecPrec 
data MilliPrec 
data NanoPrec 

data DateTime a = DateTime { days :: Int, sec :: Int, ms :: Int, ns :: Int } 

date :: Int -> DateTime DayPrec
date d = DateTime d 0 0 0

secDate :: Int -> Int -> DateTime SecPrec
secDate d s = DateTime d s 0 0

...    

--will work only for same precision which is a Good Thing (tm)
instance Eq (DateTime a) where
  (DateTime d s ms ns) == (DateTime d' s' ms' ns') = [d,s,ms,ns] == [d',s',ms',ns'] 

If I'm not mistaken, this allows me to work with one type, but to distinguish precisions if I need to. But I guess there will be some drawback as well...

Assuming that you only want to solve this problem (as opposed to a more generic one) then the decimal library might be what you want.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top