Chapter 10


module Chapter10 where

import Data.Time
import Test.Hspec

data DatabaseItem
  = DbString String
  | DbNumber Integer
  | DbDate UTCTime
  deriving (Eq, Ord, Show)

theDatabase :: [DatabaseItem]
theDatabase =
  [ DbDate (UTCTime (fromGregorian 1911 5 1) (secondsToDiffTime 34123))
  , DbNumber 9001
  , DbString "Hello, World!"
  , DbDate (UTCTime (fromGregorian 1921 5 1) (secondsToDiffTime 34123))
  , DbNumber 9003
  ]

filterDbDate :: [DatabaseItem] -> [UTCTime]
filterDbDate = foldr go []
  where
    go (DbDate date) dates = date : dates
    go _ dates = dates

filterDbNumber :: [DatabaseItem] -> [Integer]
filterDbNumber = foldr go []
  where
    go (DbNumber number) numbers = number : numbers
    go _ numbers = numbers

mostRecent :: [DatabaseItem] -> UTCTime
mostRecent = maximum . filterDbDate

sumDb :: [DatabaseItem] -> Integer
sumDb = sum . filterDbNumber

avgDb :: [DatabaseItem] -> Double
avgDb database = fromIntegral (sum numbers) / fromIntegral (length numbers)
  where
    numbers = filterDbNumber database

fibs :: [Integer]
fibs = 1 : scanl (+) 1 fibs

factorials :: [Integer]
factorials = scanl (*) 1 [2 ..]

stops = "pbtdkg"

vowels = "aeiou"

seekritFunk :: String -> Double
seekritFunk x = totalWordLength / numberOfWords
  where
    totalWordLength = fromIntegral . sum . map length . words $ x
    numberOfWords = fromIntegral . length . words $ x

myOr :: [Bool] -> Bool
myOr = foldr (||) False

myAny :: (a -> Bool) -> [a] -> Bool
myAny f = foldr ((||) . f) False

myElem :: Eq a => a -> [a] -> Bool
myElem a = foldr ((||) . (== a)) False

myElem2 :: Eq a => a -> [a] -> Bool
myElem2 = any . (==)

myReverse :: [a] -> [a]
myReverse = foldl (flip (:)) []

myMap :: (a -> b) -> [a] -> [b]
myMap f = foldr ((:) . f) []

myFilter :: (a -> Bool) -> [a] -> [a]
myFilter f =
  foldr
    (\a as ->
       if f a
         then a : as
         else as)
    []

squish :: [[a]] -> [a]
squish = foldr (++) []

squishMap :: (a -> [b]) -> [a] -> [b]
squishMap f = foldr ((++) . f) []

squishAgain :: [[a]] -> [a]
squishAgain = squishMap id

myMaximumBy :: (a -> a -> Ordering) -> [a] -> a
myMaximumBy f (a:as) =
  foldl
    (\acc a' ->
       if f acc a' == GT
         then acc
         else a')
    a
    as

myMinimumBy :: (a -> a -> Ordering) -> [a] -> a
myMinimumBy f (a:as) =
  foldl
    (\acc a' ->
       if f acc a' == LT
         then acc
         else a')
    a
    as

spec :: SpecWith ()
spec = do
  describe "database processing" $ do
    it "filterDbDate gets the dates" $
      filterDbDate theDatabase `shouldBe`
      [ UTCTime (fromGregorian 1911 5 1) (secondsToDiffTime 34123)
      , UTCTime (fromGregorian 1921 5 1) (secondsToDiffTime 34123)
      ]
    it "filterDbNumber gets the numbers" $
      filterDbNumber theDatabase `shouldBe` [9001, 9003]
    it "mostRecent gets most recent date" $
      mostRecent theDatabase `shouldBe`
      UTCTime (fromGregorian 1921 5 1) (secondsToDiffTime 34123)
    it "sumDb sums the numbers" $ sumDb theDatabase `shouldBe` 18004
    it "avgDb averages the numbers" $ avgDb theDatabase `shouldBe` 9002
  describe "scan exercises" $ do
    it "first 20 fibs" $
      take 20 fibs `shouldBe`
      [ 1
      , 1
      , 2
      , 3
      , 5
      , 8
      , 13
      , 21
      , 34
      , 55
      , 89
      , 144
      , 233
      , 377
      , 610
      , 987
      , 1597
      , 2584
      , 4181
      , 6765
      ]
    it "fibs under 100" $
      takeWhile (< 100) fibs `shouldBe` [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
    it "first 5 factorials" $ take 5 factorials `shouldBe` [1, 2, 6, 24, 120]
  describe "chapter review" $ do
    it "all stop-vowel-stop combos" $
      length
        [ [stop1, vowel, stop2]
        | stop1 <- stops
        , vowel <- vowels
        , stop2 <- stops
        ] `shouldBe`
      length stops *
      length vowels *
      length stops
    it "all p-vowel-stop combos" $
      length
        [ [stop1, vowel, stop2]
        | stop1 <- stops
        , vowel <- vowels
        , stop2 <- stops
        , stop1 == 'p'
        ] `shouldBe`
      length vowels *
      length stops
    it "calculates the average word length" $
      seekritFunk "hi there you hello" `shouldBe` 3.75
  describe "rewrite using folds" $ do
    describe "myOr" $ do
      it "empty list" $ myOr [] `shouldBe` False
      it "with a True" $ myOr [False, True] `shouldBe` True
      it "all False" $ myOr [False, False] `shouldBe` False
    describe "myAny" $ do
      it "empty list" $ myAny even [] `shouldBe` False
      it "False" $ myAny even [1, 3, 5] `shouldBe` False
      it "True" $ myAny odd [1, 3, 5] `shouldBe` True
    describe "myElem" $ do
      it "empty list" $ myElem 1 [] `shouldBe` False
      it "False" $ myElem 1 [2 .. 10] `shouldBe` False
      it "True" $ myElem 1 [1 .. 10] `shouldBe` True
    describe "myElem2" $ do
      it "empty list" $ myElem2 1 [] `shouldBe` False
      it "False" $ myElem2 1 [2 .. 10] `shouldBe` False
      it "True" $ myElem2 1 [1 .. 10] `shouldBe` True
    describe "myReverse" $ do
      it "empty" $ myReverse "" `shouldBe` ""
      it "String" $ myReverse "blah" `shouldBe` "halb"
      it "Nums" $ myReverse [1 .. 5] `shouldBe` [5, 4, 3, 2, 1]
    describe "myMap" $ do
      it "empty" $ myMap (+ 1) [] `shouldBe` []
      it "Nums" $ myMap (+ 1) [1 .. 5] `shouldBe` [2 .. 6]
    describe "myFilter" $ do
      it "empty" $ myFilter even [] `shouldBe` []
      it "evens" $ myFilter even [1 .. 5] `shouldBe` [2, 4]
      it "odds" $ myFilter odd [1 .. 5] `shouldBe` [1, 3, 5]
    describe "squish" $ do
      it "flattens" $ squish [[1 .. 3], [4 .. 6]] `shouldBe` [1 .. 6]
    describe "squishMap" $ do
      it "maps and flattens" $
        squishMap (\x -> [1, x, 3]) [2, 4] `shouldBe` [1, 2, 3, 1, 4, 3]
    describe "squishAgain" $ do
      it "flattens" $ squishAgain [[1 .. 3], [4 .. 6]] `shouldBe` [1 .. 6]
    describe "myMaximumBy" $ do
      it "gets the max" $ myMaximumBy compare [1, 53, 9001, 10] `shouldBe` 9001
      it "returns the last value that returned GT" $
        myMaximumBy (\_ _ -> GT) [1, 53, 9001, 10] `shouldBe` 1
      it "returns the last value that returned GT" $
        myMaximumBy (\_ _ -> LT) [1, 53, 9001, 10] `shouldBe` 10
    describe "myMinimumBy" $ do
      it "gets the min" $ myMinimumBy compare [1, 53, 9001, 10] `shouldBe` 1
      it "returns the last value that returned LT" $
        myMinimumBy (\_ _ -> GT) [1, 53, 9001, 10] `shouldBe` 10
      it "returns the last value that returned LT" $
        myMinimumBy (\_ _ -> LT) [1, 53, 9001, 10] `shouldBe` 1