{-| Module      : Data.List.Filter
    Description : Special takes and drops on lists
    Copyright   : (c) Preetham Gujjula, 2020
    License     : BSD3
    Maintainer  : pgujjula+list-utilities@protonmail.com
    Stability   : experimental

Special takes and drops on lists.
-}
module Data.List.Filter (
      takeEvery
    , dropEvery
    , takeUntil
    , dropUntil
    ) where

{-| @takeEvery n xs@ is a list of every @n@th element of @xs@.

    __Precondition:__ @n@ must be positive.

    >>> takeEvery 3 [1..10]
    [3, 6, 9]
    >>> takeEvery 1 [1..10] == [1..10]
    True
-}
takeEvery :: Int -> [a] -> [a]
takeEvery step xs = compute validated
  where
    compute ys = case drop (step - 1) ys of
                     []    -> []
                     y:ys' -> y : compute ys'
    validated
        | step > 0  = xs
        | otherwise = error $ "Data.List.Transform.takeEvery: Step parameter "
                           ++ "must be positive."

{-| @dropEvery n xs@ is a list of every @n@th element of @xs@.

    __Precondition:__ @n@ must be positive.

    >>> dropEvery 3 [1..10]
    [1, 2, 4, 5, 7, 8, 10]
    >>> dropEvery 1 [1..10]
    []
-}
dropEvery :: Int -> [a] -> [a]
dropEvery step xs = compute validated
  where
    compute ys = case splitAt (step - 1) ys of
                     (as, [])   -> as
                     (as, _:bs) -> as ++ compute bs
    validated
        | step > 0  = xs
        | otherwise = error $ "Data.List.Transform.dropEvery: Step parameter "
                           ++ "must be positive."

{-| Take a list until a predicate is satisfied, and include the element
    satisfying the predicate.

    >>> takeUntil (== 5) [1..]
    [1, 2, 3, 4, 5]
    >>> takeUntil (== 7) [3, 2, 1]
    [3, 2, 1]
    >>> takeUntil undefined []
    []

    Note that @takeUntil@ on a nonempty list must always yield the first
    element, and the implementation is lazy enough to take advantage of this
    fact.

    >>> head (takeUntil undefined [1..])
    1
-}
takeUntil :: (a -> Bool) -> [a] -> [a]
takeUntil _ []     = []
takeUntil _ [x]    = [x]
takeUntil f (x:xs) = x : (if f x then [] else takeUntil f xs)

{-| Drop a list until a predicate is satisfied, and don't include the element
    satisfying the predicate.

    >>> dropUntil (== 5) [1..10]
    [6, 7, 8, 9, 10]
    >>> dropUntil (< 0) [1, 2, 3]
    []
    >>> dropUntil undefined []
    []

    Note that @dropUntil@ on a nonempty list must always drop the first
    element, and the implementation is lazy enough to take advantage of this
    fact.

    >>> dropUntil undefined [undefined]
    []
-}
dropUntil :: (a -> Bool) -> [a] -> [a]
dropUntil _ []     = []
dropUntil _ [_]    = []
dropUntil f (x:xs) = if f x then xs else dropUntil f xs