diff --git a/exe-t4-commands/Commands.hs b/exe-t4-commands/Commands.hs index d366960..0345bd7 100644 --- a/exe-t4-commands/Commands.hs +++ b/exe-t4-commands/Commands.hs @@ -4,7 +4,7 @@ import T4.Data import T4.Storage import T4.Report import qualified Util as U -import Data.List +import qualified Data.Set as S import Data.Map import Data.Time import Options.Applicative @@ -82,17 +82,18 @@ addClock clock = do handle :: Command -> IO () handle (CmdIn c ts) = do cslt <- U.getCurrentSLT - addClock $ In cslt c ts + addClock $ In cslt c (S.fromList ts) handle CmdOut = do cslt <- U.getCurrentSLT addClock $ Out cslt handle CmdStatus = do clocks <- getClocks - putStrLn $ case clocks of - [] -> "No clock data yet" - cs -> summary $ last cs + putStrLn $ + if S.null clocks + then "No clock data yet" + else summary (S.findMax clocks) handle CmdCats = do clocks <- getClocks - mapM_ putStrLn (sort $ allCategories clocks) + mapM_ putStrLn $ allCategories clocks handle CmdTags = do clocks <- getClocks - mapM_ putStrLn (sort $ allTags clocks) + mapM_ putStrLn $ allTags clocks handle (CmdReport t obl man secs) = do clocks <- getClocks let durMap = (if t then tagDurations else categoryDurations) clocks @@ -101,7 +102,7 @@ handle (CmdReport t obl man secs) = do printDurMap :: Bool -> Bool -> Bool -> Map String NominalDiffTime -> IO () printDurMap o n s = mapM_ putStrLn . showDurMap o n s -getClocks :: IO [Clock] +getClocks :: IO Clocks getClocks = loadDataFromDir =<< getStorageDirectory main :: IO () diff --git a/exe-t5-interactive/TUI.hs b/exe-t5-interactive/TUI.hs index 6f3e376..c580462 100644 --- a/exe-t5-interactive/TUI.hs +++ b/exe-t5-interactive/TUI.hs @@ -7,6 +7,7 @@ import Data.Char import Data.List import Data.Maybe import Data.Function +import qualified Data.Set as S import Data.Map (Map) import Data.Time import qualified System.Console.Haskeline as H @@ -14,7 +15,7 @@ import qualified System.Console.Haskeline as H main :: IO () main = do sdir <- getStorageDirectory - clock <- lastMaybe <$> loadDataFromDir sdir + clock <- findMax <$> loadDataFromDir sdir showState clock newClock <- if isJust clock && isIn (fromJust clock) then promptIn (time $ fromJust clock) @@ -23,6 +24,7 @@ main = do Nothing -> showState clock Just c -> do addClockToDir sdir c showState (Just c) + where findMax = fmap fst . S.maxView showState :: Maybe Clock -> IO () showState = putStrLn . maybe "No clock data yet" summary @@ -68,7 +70,7 @@ clockIn = do mtags <- runWithCompletion tagsCompl $ H.getInputLine "Tags: " return $ In now (parseCat mc) (parseTags mtags) where parseCat = fmap $ dropWhile isSpace . dropWhileEnd isSpace - parseTags = map (dropWhile (== '#')) . words . fromMaybe "" + parseTags = S.fromList . map (dropWhile (== '#')) . words . fromMaybe "" report :: String -> Map String NominalDiffTime -> IO () report prefix durMap = do diff --git a/lib/Completion.hs b/lib/Completion.hs index 27a8b49..440f772 100644 --- a/lib/Completion.hs +++ b/lib/Completion.hs @@ -3,22 +3,24 @@ module Completion where import Data.Char import Data.List import Data.Function +import Data.Set (Set) +import qualified Data.Set as S import qualified System.Console.Haskeline.Completion as HC data Completion a = Compl - { complItems :: [a] + { complItems :: Set a , complToString :: a -> String } complMatch :: String -> String -> Bool complMatch = isSubsequenceOf `on` map toLower -complete :: Completion a -> String -> [a] -complete (Compl xs toStr) cs = filter (complMatch cs . toStr) xs +complete :: Completion a -> String -> Set a +complete (Compl xs toStr) cs = S.filter (complMatch cs . toStr) xs haskelineCompletions :: Completion a -> String -> [HC.Completion] haskelineCompletions c@(Compl _ toString) = - map (HC.simpleCompletion . toString) . complete c + map (HC.simpleCompletion . toString) . S.toList . complete c haskelineCompletionFunc :: Monad m => Completion a -> HC.CompletionFunc m haskelineCompletionFunc = diff --git a/lib/T4/Data.hs b/lib/T4/Data.hs index 501dc02..6209af1 100644 --- a/lib/T4/Data.hs +++ b/lib/T4/Data.hs @@ -4,8 +4,10 @@ import Data.Char import Data.Function import Data.Maybe import qualified Data.Text as T -import qualified Data.List.NonEmpty as NE -import Data.List.Extra +import Data.Set (Set) +import qualified Data.Set as S +import Data.Map (Map) +import qualified Data.Map as M import Data.Time import Data.Aeson import Data.Aeson.TH @@ -39,7 +41,7 @@ type Tag = String data Clock = In { time :: SimpleLocalTime , category :: Maybe Category - , tags :: [Tag] + , tags :: Set Tag } | Out { time :: SimpleLocalTime } @@ -68,11 +70,14 @@ summary (In t mc ts) = "IN (" ++ sltString t ++ ")" ++ catStr ++ tagsStr where catStr = maybe "" ((" [" ++) . (++ "]")) mc tagsStr = concatMap (" #" ++) ts -dayGroups :: [Clock] -> [NE.NonEmpty Clock] -dayGroups = map NE.fromList . groupOn getDay . sort +type Clocks = Set Clock -allCategories :: [Clock] -> [Category] -allCategories = nubOrd . mapMaybe category . filter isIn +dayGroups :: Clocks -> Map Day Clocks +dayGroups = foldr combine M.empty + where combine = M.insertWith S.union <$> getDay <*> S.singleton -allTags :: [Clock] -> [Tag] -allTags = nubOrd . concatMap tags . filter isIn +allCategories :: Clocks -> Set Category +allCategories = S.fromList . mapMaybe category . S.toList . S.filter isIn + +allTags :: Clocks -> Set Tag +allTags = S.unions . S.map tags . S.filter isIn diff --git a/lib/T4/Report.hs b/lib/T4/Report.hs index a161177..5bab6b0 100644 --- a/lib/T4/Report.hs +++ b/lib/T4/Report.hs @@ -3,18 +3,19 @@ module T4.Report where import T4.Data import Util import Data.List +import qualified Data.Set as S import Data.Map import Data.Time -categoryDurations :: [Clock] -> Map Category NominalDiffTime +categoryDurations :: Clocks -> Map Category NominalDiffTime categoryDurations = durations select - where select (In t (Just c) _) = ([c], getLocalTime t) - select c = ([], getLocalTime $ time c) + where select (In t (Just c) _) = (S.singleton c, getLocalTime t) + select c = (S.empty, getLocalTime $ time c) -tagDurations :: [Clock] -> Map Tag NominalDiffTime +tagDurations :: Clocks -> Map Tag NominalDiffTime tagDurations = durations select - where select (In t _ ts) = (ts, getLocalTime t) - select c = ([], getLocalTime $ time c) + where select (In t _ ts) = (ts, getLocalTime t) + select c = (S.empty, getLocalTime $ time c) showDurMap :: Bool -> Bool -> Bool -> Map String NominalDiffTime -> [String] showDurMap bySnd natural secs m = diff --git a/lib/T4/Storage.hs b/lib/T4/Storage.hs index 3ab8492..b0c2bcf 100644 --- a/lib/T4/Storage.hs +++ b/lib/T4/Storage.hs @@ -2,8 +2,8 @@ module T4.Storage where import T4.Data import Data.List -import qualified Data.List.NonEmpty as NE import Data.Maybe +import qualified Data.Set as S import Data.Yaml import Text.Regex.TDFA import System.FilePath @@ -14,23 +14,23 @@ import Control.Monad.Extra fileName :: Clock -> FilePath fileName clock = dateString (time clock) <.> "yml" -loadDataFromDir :: FilePath -> IO [Clock] +loadDataFromDir :: FilePath -> IO Clocks loadDataFromDir dir = do ymlFiles <- filter (".yml" `isSuffixOf`) <$> listDirectory dir - sort <$> concatMapM decodeFileThrow ((dir ) <$> ymlFiles) + S.fromList <$> concatMapM decodeFileThrow ((dir ) <$> ymlFiles) -writeDataToDir :: FilePath -> [Clock] -> IO () +writeDataToDir :: FilePath -> Clocks -> IO () writeDataToDir dir clocks = do forM_ (dayGroups clocks) $ \dayGroup -> do - encodeFile (dir fileName (NE.head dayGroup)) dayGroup + encodeFile (dir fileName (S.findMin dayGroup)) dayGroup addClockToDir :: FilePath -> Clock -> IO () addClockToDir dir clock = do let file = dir fileName clock - other <- ifM (doesFileExist file) - (decodeFileThrow file) - (return []) - writeDataToDir dir (clock : other) + other <- S.fromList <$> ifM (doesFileExist file) + (decodeFileThrow file) + (return []) + writeDataToDir dir $ S.insert clock other getStorageDirectoryPath :: IO FilePath getStorageDirectoryPath = do diff --git a/lib/Util.hs b/lib/Util.hs index 3c4bcf7..0b637bc 100644 --- a/lib/Util.hs +++ b/lib/Util.hs @@ -3,21 +3,20 @@ module Util where import T4.Data (SimpleLocalTime(SLT)) import Data.List import Data.Foldable -import Data.Bifunctor +import Data.Set (Set) import Data.Map (Map) import qualified Data.Map as M import Data.Time durations :: (Ord a, Show a, Foldable f) - => (entry -> ([a], LocalTime)) + => (entry -> (Set a, LocalTime)) -> f entry -> Map a NominalDiffTime durations extract xs = - let entries = sortOn snd $ extract' <$> toList xs - durs = concat $ zipWith pairDuration entries (drop 1 entries) - in foldr (uncurry $ M.insertWith (+)) M.empty durs - where extract' = first nub . extract - pairDuration (ys, t1) (_, t2) = (, diffLocalTime t2 t1) <$> ys + let entries = sortOn snd $ extract <$> toList xs + durs = zipWith pairDur entries (drop 1 entries) + in foldr (M.unionWith (+)) M.empty durs + where pairDur (ys, t1) (_, t2) = M.fromSet (const $ diffLocalTime t2 t1) ys newtype DurationConfig = DurConf { units :: [DurationUnit] } deriving (Eq, Show) @@ -69,7 +68,3 @@ showRoughDiffTime dc = showDiffTimeSplits . init . splitDiffTime dc getCurrentSLT :: IO SimpleLocalTime getCurrentSLT = SLT . zonedTimeToLocalTime <$> getZonedTime - -lastMaybe :: [a] -> Maybe a -lastMaybe [] = Nothing -lastMaybe xs = Just (last xs) diff --git a/test/CompletionSpec.hs b/test/CompletionSpec.hs index 6345a3f..04c710b 100644 --- a/test/CompletionSpec.hs +++ b/test/CompletionSpec.hs @@ -7,7 +7,9 @@ import Test.QuickCheck import Completion import Data.Char import Data.String () -import Data.List +import Data.List hiding ((\\)) +import Data.Set ((\\)) +import qualified Data.Set as S import Data.Functor.Identity import qualified System.Console.Haskeline.Completion as HC @@ -39,18 +41,20 @@ spec = do context "Completion suggestion" $ do - let items = ["foo bar", "foo baz", "qoux"] - compl = Compl (Identity <$> items) runIdentity + let items = S.fromList ["foo bar", "foo baz", "qoux"] + compl = Compl items id it "Empty -> all suggestions" $ - complete compl "" `shouldBe` (Identity <$> items) + complete compl "" `shouldBe` items it "'o' -> 3/3" $ - complete compl "o" `shouldBe` (Identity <$> items) + complete compl "o" `shouldBe` items it "'f ba' -> 2/3" $ - complete compl "f ba" `shouldBe` (Identity <$> ["foo bar", "foo baz"]) + complete compl "f ba" + `shouldBe` S.fromList ["foo bar", "foo baz"] it "'f bar' -> 1/3" $ - complete compl "f bar" `shouldBe` [Identity "foo bar"] + complete compl "f bar" + `shouldBe` S.singleton "foo bar" it "'f barz' -> 0/3" $ - complete compl "f barz" `shouldBe` [] + complete compl "f barz" `shouldBe` S.empty describe "Arbitrary completion" $ do @@ -58,14 +62,14 @@ spec = do forAll (genShortSublists $ concat aitems) $ \str -> let suggestions = complete (Compl aitems id) str in not (null suggestions) ==> - forAll (elements suggestions) $ \sugg -> + forAll (elements $ S.toList suggestions) $ \sugg -> complMatch str sugg `shouldBe` True prop "Not-suggestions don't match" $ \aitems -> forAll (genShortSublists $ concat aitems) $ \str -> let nopes = aitems \\ complete (Compl aitems id) str in not (null nopes) ==> - forAll (elements nopes) $ \nope -> + forAll (elements $ S.toList nopes) $ \nope -> complMatch str nope `shouldBe` False context "Haskeline completion" $ do @@ -73,7 +77,7 @@ spec = do describe "Completion list generation" $ do it "Basic transformation" $ - haskelineCompletions (Compl ["foo"] id) "fo" + haskelineCompletions (Compl (S.singleton "foo") id) "fo" `shouldBe` [HC.Completion "foo" "foo" True] prop "Replacement = Display" $ @@ -94,7 +98,7 @@ spec = do prop "Same completion list" $ forAll genMatchPairs $ \(compl, match) -> map HC.display (haskelineCompletions compl match) - `shouldBe` complete compl match + `shouldBe` S.toList (complete compl match) describe "Completion function transformation" $ do @@ -106,7 +110,7 @@ spec = do in runIdentity result `shouldBe` ("", compls) describe "Examples with word completion" $ do - let compl = Compl (words "foo bar baz") id + let compl = Compl (S.fromList $ words "foo bar baz") id complf = haskelineCompletionFunc compl hcompl w = HC.Completion w w True it "First word" $ runIdentity (complf ("f", "")) @@ -131,24 +135,20 @@ genShortSublists xs = do genCompletions :: Gen (Completion String) genCompletions = do ws <- listOf $ listOf $ arbitrary `suchThat` (not . isSpace) - return $ Compl ws id - -notEmpty :: [a] -> Bool -notEmpty = not . null -noEmpty :: [[a]] -> Bool -noEmpty = (&&) <$> notEmpty <*> all notEmpty + return $ Compl (S.fromList ws) id genMatches :: Completion a -> Gen String genMatches (Compl ws toString) = do - str <- (toString <$> elements ws) `suchThat` notEmpty - sublistOf str `suchThat` notEmpty + str <- (toString <$> elements (S.toList ws)) `suchThat` (not . null) + sublistOf str `suchThat` (not . null) genMatchPairs :: Gen (Completion String, String) genMatchPairs = do - compl <- genCompletions `suchThat` (noEmpty . complItems) + compl <- genCompletions `suchThat` (notEmpty . complItems) match <- genMatches compl return (compl, match) + where notEmpty = (&&) <$> not . null <*> (not . any null) instance Show (Completion a) where show (Compl items toString) = - "Compl (complItems=" ++ show (toString <$> items) ++ ")" + "Compl (complItems=" ++ show (toString <$> S.toList items) ++ ")" diff --git a/test/T4/DataSpec.hs b/test/T4/DataSpec.hs index 9c21af1..b3ad193 100644 --- a/test/T4/DataSpec.hs +++ b/test/T4/DataSpec.hs @@ -7,9 +7,10 @@ import Test.QuickCheck.Instances.Time () import T4.Data import Data.Char -import qualified Data.List.NonEmpty as NE import Data.Maybe import Data.Text (unpack, pack) +import qualified Data.Set as S +import qualified Data.Map as M import Text.Read (readMaybe) import Text.ParserCombinators.ReadP import Data.Yaml @@ -67,8 +68,8 @@ spec = do context "Clock in/out data" $ do let theTime = simpleLocalTime 2017 11 23 17 42 37 - cIn = In theTime (Just "foo") ["bar", "baz"] - cInNoCat = In theTime Nothing ["bar", "baz"] + cIn = In theTime (Just "foo") (S.fromList ["bar", "baz"]) + cInNoCat = In theTime Nothing (S.fromList ["bar", "baz"]) cOut = Out theTime describe "Predicates" $ do @@ -120,36 +121,39 @@ spec = do context "Day groups" $ do prop "Grouping in days" $ \clockLog -> not (null clockLog) ==> - forAll (elements $ dayGroups clockLog) $ \dayGroup -> - let sameDay = (== 1) . length . NE.group . NE.map getDay - in dayGroup `shouldSatisfy` sameDay + forAll (elements $ M.toList $ dayGroups clockLog) $ \(d, cs) -> + forAll (elements $ S.toList cs) $ \c -> + getDay c `shouldBe` d describe "Categories" $ do prop "Clock categories in allCategories" $ \clocks -> - let clockIns = filter isIn clocks - categories = mapMaybe category clockIns + let clockIns = S.filter isIn clocks + categories = mapMaybe category (S.toList clockIns) in not (null categories) ==> forAll (elements categories) $ \cat -> - cat `elem` allCategories clocks + cat `S.member` allCategories clocks prop "allCategories in clocks" $ \clocks -> let categories = allCategories clocks in not (null categories) ==> - forAll (elements categories) $ \cat -> - cat `elem` mapMaybe category (filter isIn clocks) + forAll (elements $ S.toList categories) $ \cat -> + let clockIns = S.filter isIn clocks + in cat `elem` mapMaybe category (S.toList clockIns) describe "Tags" $ do prop "Clock tags in allTags" $ \clocks -> - let clockIns = filter isIn clocks + let clockIns = S.filter isIn clocks in not (null clockIns) ==> - forAll (elements clockIns) $ \clock -> - not (null $ tags clock) ==> - forAll (elements $ tags clock) $ \tag -> - tag `elem` allTags clocks + forAll (elements $ S.toList clockIns) $ \clock -> + let cTags = tags clock + in not (null cTags) ==> + forAll (elements $ S.toList cTags) $ \tag -> + tag `S.member` allTags clocks prop "allTags in clocks" $ \clocks -> let theTags = allTags clocks in not (null theTags) ==> - forAll (elements theTags) $ \tag -> - tag `elem` concatMap tags (filter isIn clocks) + forAll (elements $ S.toList theTags) $ \tag -> + let cTags = S.unions . S.map tags . S.filter isIn $ clocks + in tag `S.member` cTags instance Read SimpleLocalTime where readsPrec _ = readP_to_S $ do @@ -170,6 +174,8 @@ instance Arbitrary SimpleLocalTime where instance Arbitrary Clock where arbitrary = oneof - [ In <$> arbitrary <*> arbitrary <*> listOf arbitrary + [ In <$> arbitrary + <*> arbitrary + <*> (S.fromList <$> listOf arbitrary) , Out <$> arbitrary ] diff --git a/test/T4/ReportSpec.hs b/test/T4/ReportSpec.hs index 75809be..9eae05a 100644 --- a/test/T4/ReportSpec.hs +++ b/test/T4/ReportSpec.hs @@ -7,9 +7,9 @@ import Test.QuickCheck import T4.Data import T4.Report import T4.DataSpec () -- Arbitrary Clock instance -import Data.List import Data.Function import Data.Maybe +import qualified Data.Set as S import Data.Map ((!)) import qualified Data.Map as M import Data.Time @@ -21,12 +21,13 @@ spec = do describe "Example" $ do let slt = simpleLocalTime - clocks = [ In (slt 2017 11 23 16 42 23) (Just "c1") ["t1", "t2"] - , Out (slt 2017 11 23 17 42 23) - , In (slt 2018 11 23 17 42 23) (Just "c2") ["t2"] - , In (slt 2018 11 24 17 42 23) (Just "c3") ["t3"] - , Out (slt 2018 11 24 17 43 5) - ] + clocks = S.fromList + [ In (slt 2017 11 23 16 42 23) (Just "c1") (S.fromList ["t1", "t2"]) + , Out (slt 2017 11 23 17 42 23) + , In (slt 2018 11 23 17 42 23) (Just "c2") (S.fromList ["t2"]) + , In (slt 2018 11 24 17 42 23) (Just "c3") (S.fromList ["t3"]) + , Out (slt 2018 11 24 17 43 5) + ] it "Correct category durations" $ M.toList (categoryDurations clocks) `shouldMatchList` [ ("c1", secondsToNominalDiffTime (60*60)) @@ -42,29 +43,28 @@ spec = do describe "Extraction functions" $ do prop "Category extraction" $ \(cx, cy) -> do - let clocks = [cx, cy] + let clocks = S.fromList [cx, cy] clock = minimum clocks isIn clock && isJust (category clock) ==> do let cat = fromJust (category clock) - M.keys (categoryDurations clocks) `shouldBe` [cat] + M.keysSet (categoryDurations clocks) `shouldBe` S.singleton cat prop "Tags extraction" $ \(cx, cy) -> do - let clocks = [cx, cy] + let clocks = S.fromList [cx, cy] clock = minimum clocks isIn clock ==> - M.keys (tagDurations clocks) - `shouldMatchList` nub (tags clock) + M.keysSet (tagDurations clocks) `shouldBe` tags clock prop "Category duration extraction" $ \(cx, cy) -> do let (c1, c2) = if cx <= cy then (cx, cy) else (cy, cx) isIn c1 && isJust (category c1) ==> do let cat = fromJust (category c1) diff = (diffLocalTime `on` getLocalTime . time) c2 c1 - categoryDurations [c1, c2] ! cat `shouldBe` diff + categoryDurations (S.fromList [c1, c2]) ! cat `shouldBe` diff prop "Tags duration extraction" $ \(cx, cy) -> do let (c1, c2) = if cx <= cy then (cx, cy) else (cy, cx) isIn c1 && not (null $ tags c1) ==> do let diff = (diffLocalTime `on` getLocalTime . time) c2 c1 - durs = tagDurations [c1, c2] - forAll (elements $ tags c1) $ \tag -> + durs = tagDurations $ S.fromList [c1, c2] + forAll (elements $ S.toList $ tags c1) $ \tag -> durs ! tag `shouldBe` diff describe "Text reports" $ do diff --git a/test/T4/StorageSpec.hs b/test/T4/StorageSpec.hs index f3834a5..19d14d9 100644 --- a/test/T4/StorageSpec.hs +++ b/test/T4/StorageSpec.hs @@ -8,6 +8,7 @@ import T4.Data import T4.Storage import T4.DataSpec () -- Arbitrary Clock instance import Data.List +import qualified Data.Set as S import Data.Yaml import System.FilePath import System.Directory @@ -18,9 +19,9 @@ import GHC.IO.Exception spec :: Spec spec = do - let c1 = In (simpleLocalTime 2001 2 3 4 5 6) (Just "cat1") ["t1", "t2"] + let c1 = In (simpleLocalTime 2001 2 3 4 5 6) (Just "cat1") (S.fromList ["t1", "t2"]) c2 = Out (simpleLocalTime 2001 2 3 4 5 7) - c3 = In (simpleLocalTime 2001 2 4 4 5 6) (Just "cat2") ["t2", "t3"] + c3 = In (simpleLocalTime 2001 2 4 4 5 6) (Just "cat2") (S.fromList ["t2", "t3"]) context "File names" $ do it "Simple example" $ @@ -33,11 +34,11 @@ spec = do encodeFile (tdir fileName c1) [c2, c1] encodeFile (tdir fileName c3) [c3] loadDataFromDir tdir - it "Loaded correct (sorted) clock data" $ loaded `shouldBe` [c1, c2, c3] + it "Loaded correct (sorted) clock data" $ loaded `shouldBe` S.fromList [c1, c2, c3] context "Storing data" $ do (cs1, cs2) <- runIO $ withSystemTempDirectory "t4" $ \tdir -> do - writeDataToDir tdir [c2, c1, c3] + writeDataToDir tdir $ S.fromList [c2, c1, c3] cs1 <- decodeFileThrow (tdir fileName c1) cs2 <- decodeFileThrow (tdir fileName c3) return (cs1, cs2) @@ -46,9 +47,9 @@ spec = do prop "Correct file name" $ \clock -> ioProperty $ do withSystemTempDirectory "t4" $ \tdir -> do - writeDataToDir tdir [clock] + writeDataToDir tdir $ S.singleton clock filenames <- listDirectory tdir - return $ filenames === [fileName clock] + filenames `shouldBe` [fileName clock] prop "Same file => same day" $ \clocks -> not (null clocks) ==> ioProperty $ do @@ -65,41 +66,41 @@ spec = do withSystemTempDirectory "t4" $ \tdir -> do writeDataToDir tdir clocks loaded <- loadDataFromDir tdir - return $ loaded === sort clocks + loaded `shouldBe` clocks context "Inserting single data into existing clock store" $ do describe "Simple example" $ do loaded <- runIO $ withSystemTempDirectory "t4" $ \tdir -> do - writeDataToDir tdir [c1, c3] + writeDataToDir tdir $ S.fromList [c1, c3] addClockToDir tdir c2 loadDataFromDir tdir it "Added c2 to c1's day" $ - loaded `shouldBe` [c1, c2, c3] + loaded `shouldBe` S.fromList [c1, c2, c3] prop "Empty directory: just insert" $ \clock -> ioProperty $ do withSystemTempDirectory "t4-empty" $ \tdir -> do addClockToDir tdir clock loaded <- loadDataFromDir tdir - return $ loaded === [clock] + loaded `shouldBe` S.singleton clock prop "Non-empty, different file" $ \(clocks, clock) -> - not (null clocks) && getDay clock `notElem` (getDay <$> clocks) ==> + not (null clocks) && getDay clock `notElem` S.map getDay clocks ==> ioProperty $ do withSystemTempDirectory "t4" $ \tdir -> do writeDataToDir tdir clocks addClockToDir tdir clock loaded <- loadDataFromDir tdir - return $ loaded `shouldMatchList` clock : clocks + loaded `shouldBe` S.insert clock clocks prop "Non-empty, same file" $ \(clocks, clock) -> - not (null clocks) && getDay clock `elem` (getDay <$> clocks) ==> + not (null clocks) && getDay clock `elem` S.map getDay clocks ==> ioProperty $ do withSystemTempDirectory "t4" $ \tdir -> do writeDataToDir tdir clocks addClockToDir tdir clock loaded <- loadDataFromDir tdir - return $ loaded `shouldMatchList` clock : clocks + loaded `shouldBe` S.insert clock clocks context "Storage directory on disk" $ do @@ -158,6 +159,7 @@ spec = do checks `shouldBe` (False, True, True, True) after_ (unsetEnv "T4DIR") $ -- because withEnv gets interrupted + it "Not a storage directory -> die" $ do let action = withSystemTempDirectory "dirty" $ \tdir -> do writeFile (tdir "foo.txt") "This is not T4 data" diff --git a/test/UtilSpec.hs b/test/UtilSpec.hs index 722adb2..6ea4bc0 100644 --- a/test/UtilSpec.hs +++ b/test/UtilSpec.hs @@ -10,6 +10,8 @@ import Util import Data.List import Data.Function import Data.Bifunctor +import Data.Set (Set) +import qualified Data.Set as S import Data.Map ((!)) import qualified Data.Map as M import Data.Time @@ -23,11 +25,11 @@ spec = do context "Duration helper function" $ do describe "Example" $ do - let entries = [ (['1'], lt 2017 7 17 15 42 0) - , (['2','3'], lt 2017 7 17 17 42 0) - , ([], lt 2017 7 17 20 42 0) - , (['3','3'], lt 2018 7 17 17 42 0) - , ([], lt 2021 7 17 14 42 0) + let entries = [ (S.fromList ['1'], lt 2017 7 17 15 42 0) + , (S.fromList ['2','3'], lt 2017 7 17 17 42 0) + , (S.fromList [], lt 2017 7 17 20 42 0) + , (S.fromList ['3','3'], lt 2018 7 17 17 42 0) + , (S.fromList [], lt 2021 7 17 14 42 0) ] it "Correct intervall lengths" $ M.toList (durations id entries) `shouldMatchList` @@ -38,22 +40,20 @@ spec = do describe "No usable inputs" $ do it "Empty input -> empty output" $ - durations id ([] :: [([Int], LocalTime)]) - `shouldBe` M.empty + durations id ([] :: [(Set Int, LocalTime)]) `shouldBe` M.empty prop "Singleton input -> empty output" $ \x -> - durations id [x :: ([Char], LocalTime)] `shouldBe` M.empty + durations id [x :: (Set Char, LocalTime)] `shouldBe` M.empty - prop "All the values" $ \xs -> not (null xs) ==> do - let vals = nub $ concatMap fst (init $ sortOn snd xs) - dvals = M.keys $ durations id (xs :: [([Int], LocalTime)]) - not (null dvals) ==> dvals `shouldMatchList` vals + prop "All the values" $ \xs -> not (null xs) ==> + let vals = S.unions . map fst . init . sortOn snd $ xs + in M.keysSet (durations id xs) `shouldBe` (vals :: Set Int) prop "Durations are non-negative" $ \xs -> do - let durs = M.elems $ durations id (xs :: [([Char], LocalTime)]) + let durs = M.elems $ durations id (xs :: [(Set Char, LocalTime)]) not (null durs) ==> forAll (elements durs) (`shouldSatisfy` (>= 0)) prop "Correct duration for single slots" $ \(x, y) -> do - let (a,b) = if snd x <= snd y then (x,y) else (y,x :: ([Int], LocalTime)) + let (a,b) = if snd x <= snd y then (x,y) else (y,x :: (Set Int, LocalTime)) not (null $ fst a) ==> do let diff = (diffLocalTime `on` snd) b a durs = durations id [a, b] @@ -61,7 +61,7 @@ spec = do durs ! val `shouldBe` diff prop "Order doesn't matter" $ \xs -> do - let durs = durations id (xs :: [([Int], LocalTime)]) + let durs = durations id (xs :: [(Set Int, LocalTime)]) forAll (shuffle xs) $ \ys -> durations id ys `shouldBe` durs context "Difference time splitting" $ do @@ -136,17 +136,6 @@ spec = do swords = map (\(i,s) -> show i ++ short s) splits showRoughDiffTime dc d `shouldBe` unwords swords - context "Safe list operations" $ do - - describe "last" $ do - it "Empty list -> Nothing" $ - lastMaybe ([] :: [Int]) `shouldBe` Nothing - prop "Singleton -> that element" $ \i -> - lastMaybe [i] `shouldBe` Just (i :: Int) - prop "Non-empty list -> compatible with last" $ \xs -> - not (null xs) ==> - lastMaybe xs `shouldBe` Just (last (xs :: [Int])) - instance Arbitrary DurationUnit where arbitrary = DurUnit <$> smol arbitrary <*> smol (smol arbitrary)