diff --git a/src/TaskMachine/Task.hs b/src/TaskMachine/Task.hs index 4203ac4..6052373 100644 --- a/src/TaskMachine/Task.hs +++ b/src/TaskMachine/Task.hs @@ -7,7 +7,10 @@ -- 1. A single line represents a single task -- 2. Should be entirely human-readable and easily editable using a simple text viewer/editor -- 3. When sorted alphabetically, should yield useful results --- 4. Completing a task is as simple as changing the @-@ into a @x@ +-- 4. Completing a task is as simple as changing the @"-"@ into @"x"@ +-- +-- [Incomplete task] @"-[ (\)][ d\][ c\] \"@ +-- [Complete task] @"x[\][ (\)][ d\][ c\] \"@ module TaskMachine.Task ( Task(..) @@ -19,10 +22,14 @@ module TaskMachine.Task , Snippet(..) -- * Formatting , formatTask + , formatTasks , formatDate + , formatDueDate + , formatCreationDate , formatCompletion , formatPriority , formatDescription + , formatSnippet -- * Parsing -- ** Utilities , Parser @@ -31,16 +38,16 @@ module TaskMachine.Task -- ** Objects , pTask , pTasks - , pDate , pCompletion , pPriorityChar , pPriority + , pDate , pDueDate , pCreationDate , pDescription + , pSnippet ) where -import Control.Applicative (liftA2) import Control.Monad import qualified Data.List.NonEmpty as NE import Data.Void @@ -52,6 +59,7 @@ import Text.Megaparsec.Char {- Task -} +-- | A single task data Task = Task { taskCompleted :: Completion , taskPriority :: Maybe Priority @@ -60,40 +68,70 @@ data Task = Task , taskDescription :: Description } deriving (Show) +-- | Convert a 'Task' to its string representation, which can be parsed by 'pTask'. +-- +-- If this string representation is parsed using 'pTask', it should yield the original task, +-- with a few exceptions: +-- 'taskPriority', 'taskDue' and/or 'taskCreated' might be @Nothing@, but the 'taskDescription' +-- could include the text version of these in the beginning, i. e. @taskDescription = "(A) hello"@. +-- In that case, converting the task to a string and back yields a different result. formatTask :: Task -> String formatTask t = formatCompletion (taskCompleted t) ++ " " ++ maybeWithSpace formatPriority (taskPriority t) - ++ maybeWithSpace formatDate (taskDue t) - ++ maybeWithSpace formatDate (taskCreated t) + ++ maybeWithSpace formatDueDate (taskDue t) + ++ maybeWithSpace formatCreationDate (taskCreated t) ++ formatDescription (taskDescription t) where maybeWithSpace :: (a -> String) -> Maybe a -> String maybeWithSpace _ Nothing = "" maybeWithSpace f (Just a) = f a ++ " " ---parseTasks :: FilePath -> String -> Either (ParseError Char Void) [Task] ---parseTasks = parse pTasks -- That's easy! +-- | Convert a list of tasks to its string representation, which can be parsed by 'pTasks'. +formatTasks :: [Task] -> String +formatTasks = concatMap ((++"\n") . formatTask) {- Dates -} +-- | Convert a 'Day' to @YYYY-MM-DD@ format. +-- +-- Example: @"2018-09-08"@ formatDate :: Day -> String formatDate = show +-- | Convert a 'Day' into the due date string representation, which can be parsed by 'pDueDate'. +-- +-- Example: @"d2018-09-08"@ +formatDueDate :: Day -> String +formatDueDate d = 'd' : formatDate d + +-- | Convert a 'Day into the creation date string representation, which can be parsed by 'pCreationDate'. +-- +-- Example: @"c2018-09-08"@ +formatCreationDate :: Day -> String +formatCreationDate d = 'c' : formatDate d + {- Completion -} +-- | Whether a task has been completed or not. +-- +-- May include the completion date if the task is complete. data Completion = Incomplete | Complete (Maybe Day) deriving (Show) +-- | Convert a 'Completion' to its string representation, which can be parsed by 'pCompletion'. +-- +-- Examples: @"-"@, @"x"@, @"x2018-09-08"@ formatCompletion :: Completion -> String formatCompletion Incomplete = "-" formatCompletion (Complete Nothing) = "x" -formatCompletion (Complete (Just day)) = "x " ++ formatDate day +formatCompletion (Complete (Just day)) = "x" ++ formatDate day {- Priority -} +-- | A task's priority, which can be any uppercase character from A to Z. data Priority = PrioA | PrioB | PrioC | PrioD | PrioE | PrioF | PrioG | PrioH | PrioI | PrioJ | PrioK | PrioL | PrioM | PrioN @@ -101,12 +139,17 @@ data Priority | PrioV | PrioW | PrioX | PrioY | PrioZ deriving (Bounded, Enum, Eq, Show, Ord) +-- | Convert a 'Priority' to its string representation, which can be parsed by 'pPriority'. +-- +-- Example: @"(A)"@ formatPriority :: Priority -> String formatPriority p = '(' : priorityToChar p : ")" +-- | Convert a Priority to the corresponding uppercase character from A to Z. priorityToChar :: Priority -> Char priorityToChar p = toEnum (fromEnum 'A' + fromEnum p) +-- | Convert a character to the corresponding Priority, if possible. charToPriority :: Char -> Maybe Priority charToPriority c | min_value <= value && value <= max_value = Just $ toEnum value @@ -118,32 +161,47 @@ charToPriority c {- Description -} +-- | A bunch of snippets that make up the description of a task. type Description = [Snippet] +-- | Part of a task's description. data Snippet = Word String + -- ^ A space-delimited word that is not any of the special variants listed below. | Space String + -- ^ One or more spaces that delimit words. | Project String + -- ^ A word beginning with @"+"@. | Context String + -- ^ A word beginning with @"\@"@. | KeyValue String String + -- ^ A word of the form @key:value@. + -- The key and value cannot contain any spaces. + -- The key cannot contain any @":"@ characters, but the value can. deriving (Eq, Show) +-- | Convert a 'Description' into its string representation, which can be parsed by 'pDescription'. +-- +-- Example: @"task for +project \@context"@ formatDescription :: Description -> String -formatDescription = concatMap toString - where - toString :: Snippet -> String - toString (Word s) = s - toString (Space s) = s - toString (Project s) = '+' : s - toString (Context s) = '@' : s - toString (KeyValue k v) = k ++ ":" ++ v +formatDescription = concatMap formatSnippet + +-- | Convert a 'Snippet' into its string representation, which can be parsed by 'pSnippet'. +formatSnippet :: Snippet -> String +formatSnippet (Word s) = s +formatSnippet (Space s) = s +formatSnippet (Project s) = '+' : s +formatSnippet (Context s) = '@' : s +formatSnippet (KeyValue k v) = k ++ ":" ++ v {- Parsing -} +-- | Simple megaparsec parser over 'String's. type Parser = Parsec Void String --- Completion +-- Dates +-- | Parse a date in @YYYY-MM-DD@ format (see 'formatDate'). pDate :: Parser Day pDate = label "date" $ fromGregorian <$> integer @@ -155,11 +213,24 @@ pDate = label "date" $ fromGregorian int :: Parser Int int = read <$> count 2 digitChar +-- | Parse a date in the due date format (see 'formatDueDate'). +pDueDate :: Parser Day +pDueDate = label "due date" $ char 'd' *> pDate + +-- | Parse a date in the creation date format (see 'formatCreationDate'). +pCreationDate :: Parser Day +pCreationDate = label "creation date" $ char 'c' *> pDate + +-- Completion + +-- | Parse a 'Completion' (see 'formatCompletion'). pCompletion :: Parser Completion pCompletion = Incomplete <$ char '-' - <|> char 'x' *> (Complete <$> maybeParse pDate) + <|> char 'x' *> (label "completion date" $ Complete <$> maybeParse pDate) -- Priority + +-- | Parse the priority character inside the parentheses (see 'charToPriority'). pPriorityChar :: Parser Priority pPriorityChar = do c <- anyChar @@ -168,16 +239,9 @@ pPriorityChar = do Nothing -> failure (Just $ Tokens $ c NE.:| []) (Set.singleton $ Label $ 'p' NE.:| "riority character") +-- | Parse a 'Priority' (see 'formatPriority'). pPriority :: Parser Priority -pPriority = char '(' *> pPriorityChar <* char ')' - --- Dates - -pDueDate :: Parser Day -pDueDate = char 'd' *> pDate - -pCreationDate :: Parser Day -pCreationDate = char 'c' *> pDate +pPriority = label "priority" $ char '(' *> pPriorityChar <* char ')' -- Description @@ -201,30 +265,36 @@ pKeyValue = KeyValue <$> (keyBody <* char ':') <*> wordBody where keyBody = takeWhile1P (Just "key character") (not . (`elem` ": \n")) +-- | Parse a 'Description' (see 'formatDescription'). pDescription :: Parser Description -pDescription = pSnippet +pDescription = label "description" snippet where - pEnd :: Parser Description - pEnd - = [] <$ (eof <|> void (char '\n')) - <|> pSnippet - pSnippet :: Parser Description - pSnippet - = try (liftA2 (:) pSpace pEnd) - <|> try (liftA2 (:) pProject pEnd) - <|> try (liftA2 (:) pContext pEnd) - <|> try (liftA2 (:) pKeyValue pEnd) - <|> liftA2 (:) pWord pEnd - "description" + end :: Parser Description + end = [] <$ (eof <|> void (char '\n')) <|> snippet + snippet :: Parser Description + snippet = (:) <$> pSnippet <*> end + +-- | Parse a 'Snippet' (see 'formatSnippet'). +pSnippet :: Parser Snippet +pSnippet + = try pSpace + <|> try pProject + <|> try pContext + <|> try pKeyValue + <|> pWord -- Task +-- | Parse a parser and a single space afterwards. andSpace :: Parser a -> Parser a andSpace = (<* char ' ') +-- | If the parser succeeds, return its results warpped in @Just@. +-- Otherwise, return @Nothing@. maybeParse :: Parser a -> Parser (Maybe a) maybeParse p = Just <$> try p <|> pure Nothing +-- | Parse a 'Task' (see 'formatTask'). pTask :: Parser Task pTask = Task @@ -234,5 +304,6 @@ pTask <*> maybeParse (andSpace pCreationDate) <*> pDescription +-- | Parse a list of 'Task's (see 'formatTasks'). pTasks :: Parser [Task] pTasks = many pTask <* eof