简体   繁体   中英

How do you convert a list of numbers into a list of ranges in haskell?

Say you have a list of numbers, [1,2,3,5,6,7,8,9,11,12,15,16,17]

and you want a function that takes that as an input and returns something like

[[1,3],[5,9],[11,12],[15,17]] or alternatively maybe [(1,3), (5,9), (11,12), (15,17)]

how would this be done? all of the solutions i've found online are very very long and quite convoluted, when this seems like such an easy problem for a functional language like haskell

So we have a list of numbers,

xs = [1,2,3,5,6,7,8,9,11,12,14,16,17]     -- 14 sic!

We turn it into a list of segments,

ys = [[x,x+1] | x <- xs]
  -- [[1,2], [2,3], [3,4], [5,6], ..., [11,12], [12,13], [14,15], [16,17], [17,18] ]

we join the touching segments,

zs = foldr g [] ys
  -- [[1,4], [5,10], [11,13], [14,15], [16,18]]
  where
  g [a,b] []                  =  [[a,b]]
  g [a,b] r@([c,d]:t) | b==c  =  [a,d]:t
                 | otherwise  =  [a,b]:r

and we subtract 1 from each segment's ending value,

ws = [[a,b-1] | [a,b] <- zs]
  -- [[1,3], [5,9], [11,12], [14,14], [16,17]]

All in all we get

ranges :: (Num t, Eq t) => [t] -> [[t]]
ranges  =  map (\[a,b] -> [a,b-1]) . foldr g [] . map (\x -> [x,x+1]) 
  where
  g [a,b] []                  =  [[a,b]]
  g [a,b] r@([c,d]:t) | b==c  =  [a,d]:t
                 | otherwise  =  [a,b]:r

Simple and clear.

edit: or, to be properly lazy,

  where
  g [a,b] r = [a,x]:y
       where
       (x,y) = case r of ([c,d]:t) | b==c  -> (d,t)   -- delay forcing
                         _                 -> (b,r)

update: as dfeuer notes, (a,a) type is better than [a,a] . Wherever [P,Q] appears in this code, replace it with (P,Q) . This will improve the code, with zero cost to readability.

So one might do it like the idea from @Will Ness on the stateful folding or mine under the same answer . All explanations are to be found there. Besides, if you get curious and want to read more about it then have a look at Haskell Continuation Passing Style page . I am currently trying to gerealize this in such a way that we can have a variant of foldr1 in a stateful manner. A foldS :: Foldable t => (a -> a -> b) -> ta -> b . However this is still not general stateful folding. It's just tailored to this question.

ranges :: (Ord a, Num a) => [a] -> [[a]]
ranges xs = foldr go return xs $ []
            where
            go :: (Ord a, Num a) => a -> ([a] -> [[a]]) -> ([a] -> [[a]])
            go c f = \ps -> let rrs@(r:rs) = f [c] 
                            in case ps of 
                               []  -> [c]:r:rs
                               [p] -> if p + 1 == c then rrs else [p]:(c:r):rs

*Main> ranges [1,2,3,5,6,7,8,9,11,12,15,16,17]
[[1,3],[5,9],[11,12],[15,17]]

I haven't had time to test any edge cases. All advices are welcome.

I would definitely prefer the alternative representation to the first one you give.

ranges :: (Num a, Eq a) => [a] -> [(a,a)]
ranges [] = []
ranges (a : as) = ranges1 a as

-- | A version of 'ranges' for non-empty lists, where
-- the first element is supplied separately.
ranges1 :: (Num a, Eq a) => a -> [a] -> [(a,a)]
ranges1 a as = (a, b) : bs
  where
    -- Calculate the right endpoint and the rest of the
    -- result lazily, when needed.
    (b, bs) = finish a as

-- | This takes the left end of the current interval
-- and the rest of the list and produces the right endpoint of
-- that interval and the rest of the result.
finish :: (Num a, Eq a) => a -> [a] -> (a, [(a, a)])
finish l [] = (l, [])
finish l (x : xs)
  | x == l + 1 = finish x xs
  | otherwise = (l, ranges1 x xs)

To solve the Rosetta Code problem linked in the comment above, this isn't really quite an optimal representation. I'll try to explain how to match the representation more precisely later.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM