Ajout : Makefile : %/install/test
[comptalang.git] / lib / Hcompta / Format / Ledger / Read.hs
index e7402b21ece426e195cbd8fef564ef475c76e239..8f6bf725d3f048a5bcfc220a03967bea32694b51 100644 (file)
@@ -16,6 +16,7 @@ import qualified Data.Char
 import qualified Data.Decimal
 import qualified Data.Either
 import qualified Data.List
+import           Data.List.NonEmpty (NonEmpty(..))
 import qualified Data.Map.Strict as Data.Map
 import           Data.Maybe (fromMaybe)
 import qualified Data.Time.Calendar  as Time
@@ -26,9 +27,10 @@ import           Data.Typeable ()
 import qualified Text.Parsec as R
 import           Text.Parsec (Stream, ParsecT, (<|>), (<?>))
 import qualified Data.Text.IO as Text.IO (readFile)
-import qualified Data.Text as Text (pack)
+import qualified Data.Text as Text
 import qualified System.FilePath.Posix as Path
 
+import qualified Hcompta.Calc.Balance as Calc.Balance
 import qualified Hcompta.Model.Account as Account
 import           Hcompta.Model.Account (Account)
 import qualified Hcompta.Model.Amount as Amount
@@ -47,7 +49,7 @@ import           Hcompta.Model.Date (Date)
 import           Hcompta.Format.Ledger.Journal as Journal
 import qualified Hcompta.Lib.Regex as Regex
 import           Hcompta.Lib.Regex (Regex)
-import           Hcompta.Lib.Parsec as R
+import qualified Hcompta.Lib.Parsec as R
 import qualified Hcompta.Lib.Path as Path
 
 data Context
@@ -92,9 +94,9 @@ account_name_sep = ':'
 -- | Parse an 'Account'.
 account :: Stream s m Char => ParsecT s u m Account
 account = do
-       R.notFollowedBy $ space_horizontal
+       R.notFollowedBy $ R.space_horizontal
        Account.from_List <$> do
-       many1_separated account_name $ R.char account_name_sep
+       R.many1_separated account_name $ R.char account_name_sep
 
 -- | Parse an Account.'Account.Name'.
 account_name :: Stream s m Char => ParsecT s u m Account.Name
@@ -108,11 +110,8 @@ account_name = do
                        case c of
                         _ | c == comment_begin -> R.parserZero
                         _ | c == account_name_sep -> R.parserZero
-                        _ | c == posting_type_virtual_end
-                         || c == posting_type_virtual_balanced_end ->
-                               return c <* (R.lookAhead $ R.try $ account_name_char)
-                        _ | is_space_horizontal c -> do
-                               _ <- R.notFollowedBy $ space_horizontal
+                        _ | R.is_space_horizontal c -> do
+                               _ <- R.notFollowedBy $ R.space_horizontal
                                return c <* (R.lookAhead $ R.try $
                                 ( R.try (R.char account_name_sep)
                                 <|> account_name_char
@@ -131,13 +130,13 @@ account_joker_name = do
 -- | Parse an Account.'Account.Joker'.
 account_joker :: Stream s m Char => ParsecT s u m Account.Joker
 account_joker = do
-       R.notFollowedBy $ space_horizontal
-       many1_separated account_joker_name $ R.char account_name_sep
+       R.notFollowedBy $ R.space_horizontal
+       R.many1_separated account_joker_name $ R.char account_name_sep
 
 -- | Parse a 'Regex'.
 account_regex :: Stream s m Char => ParsecT s u m Regex
 account_regex = do
-       re <- R.many1 $ R.satisfy (not . is_space_horizontal)
+       re <- R.many1 $ R.satisfy (not . R.is_space_horizontal)
        Regex.of_StringM re
 
 -- | Parse an Account.'Account.Filter'.
@@ -158,7 +157,7 @@ amount = do
        left_unit <-
                R.option Nothing $ do
                        u <- unit
-                       s <- R.many $ space_horizontal
+                       s <- R.many $ R.space_horizontal
                        return $ Just $ (u, not $ null s)
        (quantity_, style) <- do
                signing <- sign
@@ -197,8 +196,8 @@ amount = do
                 Just (u, s) ->
                        return (u, Just Style.Side_Left, Just s)
                 Nothing ->
-                       R.option (Unit.nil, Nothing, Nothing) $ do
-                               s <- R.many $ space_horizontal
+                       R.option (Unit.nil, Nothing, Nothing) $ R.try $ do
+                               s <- R.many $ R.space_horizontal
                                u <- unit
                                return $ (u, Just Style.Side_Right, Just $ not $ null s)
        return $
@@ -304,13 +303,13 @@ unit =
 directive_alias :: Stream s m Char => ParsecT s Context m ()
 directive_alias = do
        _ <- R.string "alias"
-       R.skipMany1 $ space_horizontal
+       R.skipMany1 $ R.space_horizontal
        pattern <- account_pattern
-       R.skipMany $ space_horizontal
+       R.skipMany $ R.space_horizontal
        _ <- R.char '='
-       R.skipMany $ space_horizontal
+       R.skipMany $ R.space_horizontal
        repl <- account
-       R.skipMany $ space_horizontal
+       R.skipMany $ R.space_horizontal
        case pattern of
         Account.Pattern_Exact acct -> R.modifyState $ \ctx -> ctx{context_aliases_exact=
                Data.Map.insert acct repl $ context_aliases_exact ctx}
@@ -332,7 +331,7 @@ hour_separator = R.char ':'
 
 -- | Parse a 'Date' in [YYYY/]MM/DD [HH:MM[:SS][TZ]] format.
 date :: Stream s m Char => Maybe Integer -> ParsecT s u m Date
-date def_year = do
+date def_year = (do
        n0 <- R.many1 R.digit
        day_sep <- date_separator
        n1 <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
@@ -353,7 +352,7 @@ date def_year = do
         Just day_ -> return day_
        (hour, minu, sec, tz) <-
                R.option (0, 0, 0, Time.utc) $ R.try $ do
-                       R.skipMany1 $ space_horizontal
+                       R.skipMany1 $ R.space_horizontal
                        hour <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
                        sep <- hour_separator
                        minu <- R.try (R.count 2 R.digit) <|> R.count 1 R.digit
@@ -362,7 +361,7 @@ date def_year = do
                                Just <$> (R.try (R.count 2 R.digit) <|> R.count 1 R.digit)
                        -- DO: timezone
                        tz <- R.option Time.utc $ R.try $ do
-                               R.skipMany $ space_horizontal
+                               R.skipMany $ R.space_horizontal
                                time_zone
                        return
                         ( R.integer_of_digits 10 hour
@@ -382,7 +381,7 @@ date def_year = do
                Time.ZonedTime
                 (Time.LocalTime day_ tod)
                 tz
-       <?> "date"
+       <?> "date"
 
 time_zone :: Stream s m Char => ParsecT s u m TimeZone
 time_zone =
@@ -458,22 +457,18 @@ comment_begin :: Char
 comment_begin = ';'
 
 comment :: Stream s m Char => ParsecT s u m Comment
-comment = do
+comment = (do
        _ <- R.char comment_begin
        Text.pack <$> do
        R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
-       <?> "comment"
+       <?> "comment"
 
 comments :: Stream s m Char => ParsecT s u m [Comment]
-comments = do
+comments = (do
        R.try $ do
-               R.skipMany $ R.satisfy Data.Char.isSpace
-               many1_separated comment $
-                       Text.pack <$> do
-                       R.many1 $ do
-                               R.try space_horizontal
-                               <|> (R.new_line >> space_horizontal)
+               R.many1_separated comment (R.new_line >> R.skipMany R.space_horizontal)
        <|> return []
+       ) <?> "comments"
 
 -- * Parsing 'Tag'
 
@@ -485,12 +480,12 @@ tag_sep = ','
 
 -- | Parse a 'Tag'.
 tag :: Stream s m Char => ParsecT s u m Tag
-tag = do
+tag = (do
        n <- tag_name
        _ <- R.char tag_value_sep
        v <- tag_value
        return (n, v)
-       <?> "tag"
+       <?> "tag"
 
 tag_name :: Stream s m Char => ParsecT s u m Tag.Name
 tag_name = do
@@ -502,7 +497,7 @@ tag_value = do
        Text.pack <$> do
        R.manyTill R.anyChar $ do
                R.lookAhead $ do
-                       R.try (R.char tag_sep >> R.many space_horizontal >> tag_name >> R.char tag_value_sep >> return ())
+                       R.try (R.char tag_sep >> R.many R.space_horizontal >> tag_name >> R.char tag_value_sep >> return ())
                        <|> R.try R.new_line
                        <|> R.eof
 
@@ -511,7 +506,7 @@ tags = do
        Tag.from_List <$> do
                R.many_separated tag $ do
                        _ <- R.char tag_sep
-                       R.skipMany $ space_horizontal
+                       R.skipMany $ R.space_horizontal
                        return ()
 
 not_tag :: Stream s m Char => ParsecT s u m ()
@@ -520,34 +515,43 @@ not_tag = do
                R.skipMany $ R.satisfy
                 (\c -> c /= tag_value_sep
                         && not (Data.Char.isSpace c))
-               space_horizontal
+               R.space_horizontal
 
 -- * Parsing 'Posting'
 
 -- | Parse a 'Posting'.
 posting :: Stream s m Char => ParsecT s Context m (Posting, Posting.Type)
-posting = do
+posting = (do
        ctx <- R.getState
        sourcepos <- R.getPosition
        comments_ <- comments
-       R.skipMany1 $ space_horizontal
+       R.skipMany1 $ R.space_horizontal
        status_ <- status
-       R.skipMany $ space_horizontal
-       (account_, type_) <- account_with_posting_type
+       R.skipMany $ R.space_horizontal
+       acct <- account
+       let (type_, account_) = posting_type acct
        amounts_ <-
                R.choice_try
                 [ do
-                       _ <- R.count 2 space_horizontal
-                       R.skipMany $ space_horizontal
-                       Amount.from_List <$> do
-                               R.many_separated amount $ R.try $ do
-                                       R.skipMany $ space_horizontal
+                       _ <- R.count 2 R.space_horizontal
+                       R.skipMany $ R.space_horizontal
+                       maybe id (\(u, s) ->
+                               if u == Unit.nil then id
+                               else
+                                       Data.Map.adjust (\a ->
+                                               a{ Amount.style = s{Style.precision = Style.precision $ Amount.style a}
+                                                , Amount.unit  = u })
+                                        Unit.nil)
+                        (context_unit_and_style ctx) .
+                        Amount.from_List <$> do
+                               R.many_separated amount $ do
+                                       R.skipMany $ R.space_horizontal
                                        _ <- R.char amount_sep
-                                       R.skipMany $ space_horizontal
+                                       R.skipMany $ R.space_horizontal
                                        return ()
                 , return Data.Map.empty
-                ]
-       R.skipMany $ space_horizontal
+                ] <?> "amounts"
+       R.skipMany $ R.space_horizontal
        -- TODO: balance assertion
        -- TODO: conversion
        comments__ <- (comments_ ++) <$> comments
@@ -575,7 +579,7 @@ posting = do
         , Posting.status=status_
         , Posting.tags=tags_
         }, type_)
-       <?> "posting"
+       <?> "posting"
 
 amount_sep :: Char
 amount_sep = '+'
@@ -588,26 +592,69 @@ tags_of_comments =
         . R.runParser (not_tag >> tags <* R.eof) () "" )
 
 status :: Stream s m Char => ParsecT s u m Transaction.Status
-status =
+status = (do
        ( R.try $ do
-               R.skipMany $ space_horizontal
-               _ <- (R.char '*' <|> R.char '!') <?> "status"
+               R.skipMany $ R.space_horizontal
+               _ <- (R.char '*' <|> R.char '!')
                return True )
        <|> return False
-       <?> "status"
-
--- | Parse an 'Account' with Posting.'Posting.Type'.
-account_with_posting_type :: Stream s m Char => ParsecT s u m (Account, Posting.Type)
-account_with_posting_type = do
-       R.choice_try
-        [ (, Posting.Type_Virtual)          <$> R.between (R.char posting_type_virtual_begin)
-                                                          (R.char posting_type_virtual_end)
-                                                          account
-        , (, Posting.Type_Virtual_Balanced) <$> R.between (R.char posting_type_virtual_balanced_begin)
-                                                          (R.char posting_type_virtual_balanced_end)
-                                                          account
-        , (, Posting.Type_Regular)          <$> account
-        ]
+       ) <?> "status"
+
+-- | Return the Posting.'Posting.Type' and stripped 'Account' of the given 'Account'.
+posting_type :: Account -> (Posting.Type, Account)
+posting_type acct =
+       fromMaybe (Posting.Type_Regular, acct) $ do
+               case acct of
+                name:|[] ->
+                       case Text.stripPrefix virtual_begin name of
+                        Just name' -> do
+                               name'' <-
+                                           Text.stripSuffix virtual_end name'
+                                       >>= return . Text.strip
+                               guard $ not $ Text.null name''
+                               Just (Posting.Type_Virtual, name'':|[])
+                        Nothing -> do
+                               name' <-
+                                           Text.stripPrefix virtual_balanced_begin name
+                                       >>= Text.stripSuffix virtual_balanced_end
+                                       >>= return . Text.strip
+                               guard $ not $ Text.null name'
+                               Just (Posting.Type_Virtual_Balanced, name':|[])
+                first_name:|acct' -> do
+                               let rev_acct' = Data.List.reverse acct'
+                               let last_name = Data.List.head rev_acct'
+                               case Text.stripPrefix virtual_begin first_name
+                                       >>= return . Text.stripStart of
+                                Just first_name' -> do
+                                       last_name' <-
+                                               Text.stripSuffix virtual_end last_name
+                                               >>= return . Text.stripEnd
+                                       guard $ not $ Text.null first_name'
+                                       guard $ not $ Text.null last_name'
+                                       Just $
+                                               ( Posting.Type_Virtual
+                                               , first_name':|
+                                                       Data.List.reverse (last_name':Data.List.tail rev_acct')
+                                               )
+                                Nothing -> do
+                                       first_name' <-
+                                               Text.stripPrefix virtual_balanced_begin first_name
+                                               >>= return . Text.stripStart
+                                       last_name'  <-
+                                               Text.stripSuffix virtual_balanced_end last_name
+                                               >>= return . Text.stripEnd
+                                       guard $ not $ Text.null first_name'
+                                       guard $ not $ Text.null last_name'
+                                       Just $
+                                               ( Posting.Type_Virtual_Balanced
+                                               , first_name':|
+                                                       Data.List.reverse (last_name':Data.List.tail rev_acct')
+                                               )
+       where
+               virtual_begin          = Text.singleton posting_type_virtual_begin
+               virtual_end            = Text.singleton posting_type_virtual_end
+               virtual_balanced_begin = Text.singleton posting_type_virtual_balanced_begin
+               virtual_balanced_end   = Text.singleton posting_type_virtual_balanced_end
 
 posting_type_virtual_begin :: Char
 posting_type_virtual_begin = '('
@@ -621,45 +668,50 @@ posting_type_virtual_balanced_end = ']'
 -- * Parsing 'Transaction'
 
 transaction :: Stream s m Char => ParsecT s Context m Transaction
-transaction = do
+transaction = (do
        sourcepos <- R.getPosition
        ctx <- R.getState
        comments_before <- comments
        date_ <- date (Just $ context_year ctx)
        dates_ <-
                R.option [] $ R.try $ do
-                       R.skipMany $ space_horizontal
+                       R.skipMany $ R.space_horizontal
                        _ <- R.char date_sep
-                       R.skipMany $ space_horizontal
+                       R.skipMany $ R.space_horizontal
                        R.many_separated
                         (date (Just $ context_year ctx)) $
                                R.try $ do
-                                       R.many $ space_horizontal
+                                       R.many $ R.space_horizontal
                                        >> R.char date_sep
-                                       >> (R.many $ space_horizontal)
-       R.skipMany $ space_horizontal
+                                       >> (R.many $ R.space_horizontal)
+       R.skipMany $ R.space_horizontal
        status_ <- status
        code_ <- R.option "" $ R.try code
-       R.skipMany $ space_horizontal
+       R.skipMany $ R.space_horizontal
        description_ <- description
-       R.skipMany $ space_horizontal
+       R.skipMany $ R.space_horizontal
        comments_after <- comments
        let tags_ =
                Data.Map.unionWith (++)
                 (tags_of_comments comments_before)
                 (tags_of_comments comments_after)
        R.new_line
-       postings_ <- many1_separated posting R.new_line
-       let (postings, postings__) =
-               (Posting.from_List . Data.List.map fst) *** id $
-               Data.List.partition
-                ((Posting.Type_Regular ==) . snd)
-                postings_
-       let (virtual_postings, balanced_virtual_postings) =
+       (postings_unchecked, postings_not_regular) <-
+               ((Posting.from_List . Data.List.map fst) *** id) .
+               Data.List.partition ((Posting.Type_Regular ==) . snd) <$>
+               R.many1_separated posting R.new_line
+       let (virtual_postings, balanced_virtual_postings_unchecked) =
                join (***) (Posting.from_List . Data.List.map fst) $
-               Data.List.partition
-                ((Posting.Type_Virtual ==) . snd)
-                postings__
+               Data.List.partition ((Posting.Type_Virtual ==) . snd)
+                postings_not_regular
+       postings <-
+               case Calc.Balance.infer_equilibre postings_unchecked of
+                Left _l -> fail $ "transaction not-equilibrated"
+                Right ps -> return ps
+       balanced_virtual_postings <-
+               case Calc.Balance.infer_equilibre balanced_virtual_postings_unchecked of
+                Left _l -> fail $ "virtual transaction not-equilibrated"
+                Right ps -> return ps
        return $
                Transaction.Transaction
                 { Transaction.code=code_
@@ -674,56 +726,58 @@ transaction = do
                 , Transaction.status=status_
                 , Transaction.tags=tags_
                 }
-       <?> "transaction"
+       <?> "transaction"
 
 date_sep :: Char
 date_sep = '='
 
 code :: Stream s m Char => ParsecT s Context m Transaction.Code
-code = do
+code = (do
        Text.pack <$> do
-       R.skipMany $ space_horizontal
+       R.skipMany $ R.space_horizontal
        R.between (R.char '(') (R.char ')') $
-               R.many $ R.satisfy (\c -> c /= ')' && not (is_space_horizontal c))
-       <?> "code"
+               R.many $ R.satisfy (\c -> c /= ')' && not (R.is_space_horizontal c))
+       <?> "code"
 
 description :: Stream s m Char => ParsecT s u m Transaction.Description
-description = do
+description = (do
        Text.pack <$> do
        R.many $ R.try description_char
-       <?> "description"
+       <?> "description"
        where
                description_char :: Stream s m Char => ParsecT s u m Char
                description_char = do
                        c <- R.anyChar
                        case c of
                         _ | c == comment_begin -> R.parserZero
-                        _ | is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
+                        _ | R.is_space_horizontal c -> return c <* (R.lookAhead $ R.try $ description_char)
                         _ | not (Data.Char.isSpace c) -> return c
                         _ -> R.parserZero
 
 -- * Parsing directives
 
 default_year :: Stream s m Char => ParsecT s Context m ()
-default_year = do
+default_year = (do
        year <- R.integer_of_digits 10 <$> R.many1 R.digit
+       R.skipMany R.space_horizontal >> R.new_line
        context_ <- R.getState
        R.setState context_{context_year=year}
+       ) <?> "default year"
 
 default_unit_and_style :: Stream s m Char => ParsecT s Context m ()
-default_unit_and_style = do
-       R.skipMany1 space_horizontal
+default_unit_and_style = (do
        amount_ <- amount
-       R.skipMany space_horizontal >> R.new_line >> R.skipMany space_horizontal
+       R.skipMany R.space_horizontal >> R.new_line
        context_ <- R.getState
-       R.setState context_{context_unit_and_style=Just $
-        ( Amount.unit  amount_
-        , Amount.style amount_ )}
+       R.setState context_{context_unit_and_style =
+               Just $
+                ( Amount.unit  amount_
+                , Amount.style amount_ )}
+       ) <?> "default unit and style"
 
 include :: Stream s IO Char => ParsecT s Context IO ()
-include = do
+include = (do
        sourcepos <- R.getPosition
-       R.skipMany1 $ space_horizontal
        filename <- R.manyTill R.anyChar (R.lookAhead R.new_line <|> R.eof)
        context_ <- R.getState
        let journal_ = context_journal context_
@@ -749,12 +803,12 @@ include = do
                        journal_{Journal.includes=
                                journal_included{Journal.file=file_}
                                : Journal.includes journal_}}
-       <?> "include"
+       <?> "include"
 
 -- * Parsing 'Journal'
 
 journal :: Stream s IO Char => FilePath -> ParsecT s Context IO Journal
-journal file_ = do
+journal file_ = (do
        currentLocalTime <- liftIO $
                Time.utcToLocalTime
                <$> Time.getCurrentTimeZone
@@ -763,32 +817,34 @@ journal file_ = do
        context_ <- R.getState
        R.setState $ context_{context_year=currentLocalYear}
        journal_rec file_
-       <?> "journal"
+       <?> "journal"
 
 journal_rec :: Stream s IO Char => FilePath -> ParsecT s Context IO Journal
 journal_rec file_ = do
        last_read_time <- liftIO $ Time.getCurrentTime
        R.skipMany $ do
-               R.skipMany1 R.space
-               <|> ((R.choice_try
-                        [ R.string "Y" >> return default_year
-                        , R.string "D" >> return default_unit_and_style
+               R.choice_try
+                [ R.skipMany1 R.space
+                , (do (R.choice_try
+                        [ R.string "Y"        >> return default_year
+                        , R.string "D"        >> return default_unit_and_style
                         , R.string "!include" >> return include
-                        ] <?> "directive") >>= id)
-               <|> do
-               t <- transaction
-               context_' <- R.getState
-               let j = context_journal context_'
-               R.setState $ context_'{context_journal=
-                       j{Journal.transactions=
-                               Data.Map.insertWith (flip (++))
-                                -- NOTE: flip-ing preserves order but slows down
-                                -- when many transactions have the very same date.
-                                (Date.to_UTC $ fst $ Transaction.dates t) [t]
-                                (Journal.transactions j)}}
-               R.new_line <|> R.eof
-       
-       R.skipMany $ R.satisfy Data.Char.isSpace
+                        ] <?> "directive")
+                               >>= \r -> R.skipMany1 R.space_horizontal >> r)
+                , ((do
+                               t <- transaction
+                               context_' <- R.getState
+                               let j = context_journal context_'
+                               R.setState $ context_'{context_journal=
+                                       j{Journal.transactions=
+                                               Data.Map.insertWith (flip (++))
+                                                -- NOTE: flip-ing preserves order but slows down
+                                                -- when many transactions have the very same date.
+                                                (Date.to_UTC $ fst $ Transaction.dates t) [t]
+                                                (Journal.transactions j)}}
+                               R.new_line <|> R.eof))
+                , R.try (comment >> return ())
+                ]
        R.eof
        journal_ <- context_journal <$> R.getState
        return $