map - change each element
fold - combine all the elements
scan - combine all the elements keeping a running "total" - this is what you need
It's going to be easier to keep everything as an Integer until the very end:
type PosixOffset = Integer
A string in your listOfTimes could be a unix time, an increment or an erroneous value.
We could represent that by Maybe (Either PosixOffset Integer) but that could get annoying.
Let's roll our own:
data Time = Unix PosixOffset | Inc Integer | Error String deriving Show
This allows me to be flexible about what we do later with an error: crash the program with an error,
show the Error message to the user but somehow allow them to resume, or ignore the bad value.
Let's make safe version to replace read :: String -> Integer, which returns Nothing instead of crashing. We'll need to import Data.Char (isDigit)
readInteger :: String -> Maybe Integer
readInteger "" = Nothing
readInteger xs | all isDigit xs = Just (read xs)
| otherwise = Nothing
Now we can use that to readTime with some helpful Error messages.
readTime :: String -> Time
readTime ('u':xs) = case readInteger xs of
Just i -> Unix i
Nothing -> Error $ "readTime: there should be an integer after the u, but I got: " ++ 'u':xs
readTime [] = Error "readTime: empty time"
readTime xs = case readInteger xs of
Just i -> Inc i
Nothing -> Error $ "readTime: " ++ xs ++ " is neither a unix time nor an increment."
The plan is to convert our list of Strings to a list of pairs (PosixOffset,Integer),
with the last known PosixOffset from a unix time, and the current increment.
We'll then need to be able to convert these pairs to a UTCTime
toUTC :: (PosixOffset,Integer) -> UTCTime
toUTC (p,i) = psUTC (p+i)
Now we need to know how to combine the running total of the Times with the next Time. We'll keep hold of the last unix time for reference.
addTime :: (PosixOffset,Integer) -> Time -> (PosixOffset,Integer)
addTime (oldunix,oldinc) time = case time of
Unix new -> (new,0) -- If there's a new unix time, replace and reset the inc to 0.
Inc inc -> (oldunix,inc) -- If there's a new increment, replace the old one.
Error msg -> error msg -- If there's an error, crash showing it.
or you could use
addTimeTolerant :: (PosixOffset,Integer) -> Time -> (PosixOffset,Integer)
addTimeTolerant (oldunix,oldinc) time = case time of
Unix new -> (new,0) -- If there's a new unix time, replace and reset the inc to 0.
Inc inc -> (oldunix,inc) -- If there's a new increment, replace the old one.
Error msg -> (oldunix,oldinc) -- If there's an error, ignore it and keep the time the same.
Now we can stick it together: turn the Strings into Times,
then combine them into (PosixOffset,Integer) pairs by scanning with addTime,
then turn all the resulting pairs into UTCTimes.
runningTotal :: [String] -> [UTCTime]
runningTotal [] = []
runningTotal xss = let (t:ts) = map readTime xss in -- turn Strings to Times
case t of
Error msg -> error msg
Inc _ -> error "runningTotal: list must start with a unix time"
Unix po -> map toUTC $ scanl addTime (po,0) ts -- scan the list adding times,
-- starting with an initial unix time
-- then convert them all to UTC
or if you like the keep calm and carry on approach of addTimeTolerant, you could use
isn't_UnixTime :: Time -> Bool
isn't_UnixTime (Unix _) = False
isn't_UnixTime _ = True
runningTotalTolerant :: [String] -> [UTCTime]
runningTotalTolerant xss =
let ts = dropWhile isn't_UnixTime (map readTime xss) in -- cheerily find the first unix time
if null ts then [] else -- if there wasn't one, there are no UTCTimes
let (Unix po) = head ts in -- grab the first time
map toUTC $ scanl addTimeTolerant (po,0) (tail ts) -- scan the list adding times,
-- starting with an initial unix time
-- then convert them all to UTC