{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-incomplete-patterns #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE LambdaCase #-} module OrgMode where import XMonad.Prelude hiding ((!?)) import XMonad.Prompt.OrgMode import qualified Data.Map.Strict as Map import Data.Map.Strict (Map, (!), (!?)) import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck spec :: Spec spec = do prop "prop_encodePreservation" prop_encodePreservation prop "prop_decodePreservation" prop_decodePreservation -- Checking for regressions describe "pInput" $ do it "works with todo +d 22 january 2021" $ do pInput "todo +d 22 ja 2021" `shouldBe` Just ( Deadline "todo" (Time {date = Date (22, Just 1, Just 2021), tod = Nothing}) NoPriority ) it "works with todo +d 22 01 2022" $ do pInput "todo +d 22 01 2022" `shouldBe` Just ( Deadline "todo" (Time {date = Date (22, Just 1, Just 2022), tod = Nothing}) NoPriority ) it "works with todo +d 1 01:01" $ do pInput "todo +d 1 01:01" `shouldBe` Just ( Deadline "todo" (Time {date = Date (1, Nothing, Nothing), tod = Just $ TimeOfDay 1 1}) NoPriority ) it "works with todo +d 22 jan 2021 01:01 #b" $ do pInput "todo +d 22 jan 2021 01:01 #b" `shouldBe` Just ( Deadline "todo" (Time {date = Date (22, Just 1, Just 2021), tod = Just $ TimeOfDay 1 1}) B ) context "no priority#b" $ do it "parses to the correct thing" $ pInput "no priority#b" `shouldBe` Just (NormalMsg "no priority#b" NoPriority) it "encode" $ prop_encodePreservation (OrgMsg "no priority#b") it "decode" $ prop_decodePreservation (NormalMsg "no priority#b" NoPriority) context "+d +d f" $ do it "encode" $ prop_encodePreservation (OrgMsg "+d +d f") it "decode" $ prop_decodePreservation (Deadline "+d" (Time {date = Next Friday, tod = Nothing}) NoPriority) context "+d f 1 +d f #c" $ do it "encode" $ prop_encodePreservation (OrgMsg "+d +d f") it "decode" $ prop_decodePreservation (Deadline "+d" (Time {date = Next Friday, tod = Nothing}) C) context "+d f 1 +d f" $ do it "encode" $ prop_encodePreservation (OrgMsg "+d f 1 +d f") it "decode" $ prop_decodePreservation (Deadline "+d f 1" (Time {date = Next Friday, tod = Nothing}) NoPriority) context "+d f 1 +d f #b" $ do it "encode" $ prop_encodePreservation (OrgMsg "+d f 1 +d f #b") it "decode" $ prop_decodePreservation (Deadline "+d f 1" (Time {date = Next Friday, tod = Nothing}) B) -- | Parsing preserves all info that printing does. prop_encodePreservation :: OrgMsg -> Property prop_encodePreservation (OrgMsg s) = pInput s === (pInput . ppNote =<< pInput s) -- | Printing preserves all info that parsing does. prop_decodePreservation :: Note -> Property prop_decodePreservation n = Just (ppNote n) === (fmap ppNote . pInput $ ppNote n) ------------------------------------------------------------------------ -- Pretty Printing ppNote :: Note -> String ppNote = \case Scheduled str t p -> str <> " +s " <> ppTime t <> ppPrio p Deadline str t p -> str <> " +d " <> ppTime t <> ppPrio p NormalMsg str p -> str <> ppPrio p ppPrio :: Priority -> String ppPrio = \case NoPriority -> "" prio -> " #" <> show prio ppTime :: Time -> String ppTime (Time d t) = ppDate d <> ppTOD t where ppTOD :: Maybe TimeOfDay -> String ppTOD = maybe "" ((' ' :) . show) ppDate :: Date -> String ppDate dte = case days !? dte of Just v -> v Nothing -> case d of -- only way it can't be in the map Date (d', mbM, mbY) -> show d' <> maybe "" ((' ' :) . (months !)) mbM <> maybe "" ((' ' :) . show) mbY ------------------------------------------------------------------------ -- Arbitrary Instances -- | An arbitrary (correct) message string. newtype OrgMsg = OrgMsg String deriving (Show) instance Arbitrary OrgMsg where arbitrary :: Gen OrgMsg arbitrary = OrgMsg <$> randomString -- note <<>> elements [" +s ", " +d ", ""] <<>> dateGen <<>> hourGen -- time and date <<>> elements ("" : map (reverse . (: " #")) "AaBbCc") -- priority where dateGen :: Gen String dateGen = oneof [ pure $ days ! Today , pure $ days ! Tomorrow , elements $ (days !) . Next <$> [Monday .. Sunday] , rNat -- 17 , unwords <$> sequenceA [rNat, monthGen] -- 17 jan , unwords <$> sequenceA [rNat, monthGen, rYear] -- 17 jan 2021 , unwords <$> traverse (fmap show) [rNat, rMonth] -- 17 01 , unwords <$> traverse (fmap show) [rNat, rMonth, rYear] -- 17 01 2021 ] where rNat, rYear, rMonth :: Gen String rNat = show <$> posInt rMonth = show <$> posInt `suchThat` (<= 12) rYear = show <$> posInt `suchThat` (> 25) monthGen :: Gen String monthGen = elements $ Map.elems months hourGen :: Gen String hourGen = oneof [ pure " " <<>> (pad <$> hourInt) <<>> pure ":" <<>> (pad <$> minuteInt) , pure "" ] where pad :: Int -> String pad n = (if n <= 9 then "0" else "") <> show n instance Arbitrary Note where arbitrary :: Gen Note arbitrary = do msg <- randomString t <- arbitrary p <- arbitrary elements [Scheduled msg t p, Deadline msg t p, NormalMsg msg p] instance Arbitrary Priority where arbitrary :: Gen Priority arbitrary = elements [A, B, C, NoPriority] instance Arbitrary Time where arbitrary :: Gen Time arbitrary = Time <$> arbitrary <*> arbitrary instance Arbitrary Date where arbitrary :: Gen Date arbitrary = oneof [ pure Today , pure Tomorrow , Next . toEnum <$> choose (0, 6) , do d <- posInt m <- mbPos `suchThat` (<= Just 12) Date . (d, m, ) <$> if isNothing m then pure Nothing else mbPos `suchThat` (>= Just 25) ] instance Arbitrary TimeOfDay where arbitrary :: Gen TimeOfDay arbitrary = TimeOfDay <$> hourInt <*> minuteInt ------------------------------------------------------------------------ -- Util randomString :: Gen String randomString = listOf arbitraryPrintableChar <<>> (noSpace <&> (: [])) where noSpace :: Gen Char noSpace = arbitraryPrintableChar `suchThat` (/= ' ') days :: Map Date String days = Map.fromList [ (Today, "tod"), (Tomorrow, "tom"), (Next Monday, "m"), (Next Tuesday, "tu") , (Next Wednesday, "w"), (Next Thursday, "th"), (Next Friday, "f") , (Next Saturday,"sa"), (Next Sunday,"su") ] months :: Map Int String months = Map.fromList [ (1, "ja"), (2, "f"), (3, "mar"), (4, "ap"), (5, "may"), (6, "jun") , (7, "jul"), (8, "au"), (9, "s"), (10, "o"), (11, "n"), (12, "d") ] posInt :: Gen Int posInt = getPositive <$> arbitrary @(Positive Int) hourInt :: Gen Int hourInt = posInt `suchThat` (<= 23) minuteInt :: Gen Int minuteInt = posInt `suchThat` (<= 59) mbPos :: Num a => Gen (Maybe a) mbPos = fmap (fromIntegral . getPositive) <$> arbitrary @(Maybe (Positive Int)) infixr 6 <<>> (<<>>) :: (Applicative f, Monoid a) => f a -> f a -> f a (<<>>) = liftA2 (<>)