Reputation: 407
I have a list of strings, the strings are either a unixtime or an increment from that unixtime eg.
listOfTimes :: [String]
listOfTimes = ["u1345469400","1","2","3","4","5","6","u1346427334","1","2","3","4","5","6"]
I have written functions which take a unixtime and return a UTCTime
dateStringToUTC :: [Char] -> UTCTime
dateStringToUTC a = out
where
asInt = read (tail a) :: Integer
out = psUTC asInt
Or take an increment and the last unixtime and return a UTCTime
incToUTC :: [Char] -> String -> UTCTime
incToUTC a b = madeDate
where
madeDate = psUTC posixOffset
posixOffset = lastTime + incTime
lastTime = read (tail a) :: Integer
incTime = read b :: Integer
However I can't think of a way to write a function that I can map across the entire list that returns a [UTCTime]
Upvotes: 2
Views: 167
Reputation: 32455
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 Time
s 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 String
s into Time
s,
then combine them into (PosixOffset,Integer)
pairs by scan
ning with addTime
,
then turn all the resulting pairs into UTCTime
s.
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
Upvotes: 0
Reputation: 102066
Another way would be to collect the times that correspond to each other into a separate list, and deal with them separately, i.e.
convertUTCs [] = []
convertUTCs (x:xs) = map (incToUTC x) increments ++ convertUTCs rest
where
(increments, rest) = break (\str -> head str == 'u') xs
This takes the first element (which should always be of the form "u12345"
) and all the increments for that time (i.e. the elements that don't start with 'u'
), and then does the processing on them.
Upvotes: 1
Reputation: 47052
timesToUnixTimes :: [String] -> [UTCTime]
As ja points out, this is not a simple map
. But the final step of converting a [Integer]
to a [UTCTime]
is a map
:
timesToUnixTimes (s : ss) = map psUTC (i : is)
where
The first element of the input list, s
, had better be a unixtime:
i = read (tail s) :: Integer
Subsequent elements, ss
, may be either, so the decoding function needs access to the previous element of the output list:
is = zipWith timeToInteger ss (i : is)
Writing timeToInteger :: String -> Integer -> Integer
is left as an exercise.
Two points from this:
You can think of zipWith
as mapping a function over two lists at a time (similarly, zipWith3
maps a function over three lists at a time, zipWith4
maps over four lists at a time, etc; there isn't a function called zipWith1
because it's called map
).
is
appears in its own definition. This works thanks to laziness non-strictness.
is
depends on the first element of ss
and on i
.is
depends on the second element of ss
and on the first element of is
.is
depends on the third element of ss
and on the second element of is
.No element of is
depends on itself, or on a later element of is
.
Upvotes: 0
Reputation: 77384
As ja's answer says, this is not a simple map. A general fold would work, but that's true of any list operation.
What you're trying to do here sounds more specifically like a use for scanr
, which is a right fold that produces a list of each intermediate step rather than just the final result. In your case, the accumulator would be the previous time value, and at each step you'd either add an increment or replace it with a new time. The output would be a (lazy!) list of each computed time.
Upvotes: 4
Reputation: 4249
It's not a map, because you have 2 parameters to your inc function - you're using a previous list element in subsequent calls. Look into folds: foldl
, foldr
, etc.
Upvotes: 1