lib: Remove non-law-abiding Monoid instance for Journal.
This commit is contained in:
		
							parent
							
								
									702c958487
								
							
						
					
					
						commit
						e0dde6fe57
					
				@ -90,6 +90,7 @@ import Control.Monad.Extra
 | 
			
		||||
import Control.Monad.Reader as R
 | 
			
		||||
import Control.Monad.ST
 | 
			
		||||
import Data.Array.ST
 | 
			
		||||
import Data.Default (Default(..))
 | 
			
		||||
import Data.Function ((&))
 | 
			
		||||
import qualified Data.HashTable.ST.Cuckoo as H
 | 
			
		||||
import Data.List
 | 
			
		||||
@ -97,9 +98,8 @@ import Data.List.Extra (groupSort, nubSort)
 | 
			
		||||
import qualified Data.Map as M
 | 
			
		||||
import Data.Maybe
 | 
			
		||||
#if !(MIN_VERSION_base(4,11,0))
 | 
			
		||||
import Data.Monoid
 | 
			
		||||
import Data.Semigroup (Semigroup(..))
 | 
			
		||||
#endif
 | 
			
		||||
import qualified Data.Semigroup as Sem
 | 
			
		||||
import qualified Data.Set as S
 | 
			
		||||
import Data.Text (Text)
 | 
			
		||||
import qualified Data.Text as T
 | 
			
		||||
@ -157,7 +157,7 @@ instance Show Journal where
 | 
			
		||||
--                      ,show $ map fst $ jfiles j
 | 
			
		||||
--                      ]
 | 
			
		||||
 | 
			
		||||
-- The monoid instance for Journal is useful for two situations.
 | 
			
		||||
-- The semigroup instance for Journal is useful for two situations.
 | 
			
		||||
--
 | 
			
		||||
-- 1. concatenating finalised journals, eg with multiple -f options:
 | 
			
		||||
-- FIRST <> SECOND. The second's list fields are appended to the
 | 
			
		||||
@ -168,7 +168,9 @@ instance Show Journal where
 | 
			
		||||
-- CHILD <> PARENT. A parsed journal's data is in reverse order, so
 | 
			
		||||
-- this gives what we want.
 | 
			
		||||
--
 | 
			
		||||
instance Sem.Semigroup Journal where
 | 
			
		||||
-- Note that (<>) is right-biased, so nulljournal is only a left identity.
 | 
			
		||||
-- In particular, this prevents Journal from being a monoid.
 | 
			
		||||
instance Semigroup Journal where
 | 
			
		||||
  j1 <> j2 = Journal {
 | 
			
		||||
     jparsedefaultyear          = jparsedefaultyear          j2
 | 
			
		||||
    ,jparsedefaultcommodity     = jparsedefaultcommodity     j2
 | 
			
		||||
@ -190,12 +192,8 @@ instance Sem.Semigroup Journal where
 | 
			
		||||
    ,jlastreadtime              = max (jlastreadtime j1) (jlastreadtime j2)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
instance Monoid Journal where
 | 
			
		||||
  mempty = nulljournal
 | 
			
		||||
#if !(MIN_VERSION_base(4,11,0))
 | 
			
		||||
  -- This is redundant starting with base-4.11 / GHC 8.4.
 | 
			
		||||
  mappend = (Sem.<>)
 | 
			
		||||
#endif
 | 
			
		||||
instance Default Journal where
 | 
			
		||||
  def = nulljournal
 | 
			
		||||
 | 
			
		||||
nulljournal :: Journal
 | 
			
		||||
nulljournal = Journal {
 | 
			
		||||
 | 
			
		||||
@ -48,22 +48,24 @@ import Control.Arrow (right)
 | 
			
		||||
import qualified Control.Exception as C
 | 
			
		||||
import Control.Monad (when)
 | 
			
		||||
import "mtl" Control.Monad.Except (runExceptT)
 | 
			
		||||
import Data.Default
 | 
			
		||||
import Data.Default (def)
 | 
			
		||||
import Data.Foldable (asum)
 | 
			
		||||
import Data.List
 | 
			
		||||
import Data.Maybe
 | 
			
		||||
import Data.Ord
 | 
			
		||||
import Data.List (group, sort, sortBy)
 | 
			
		||||
import Data.List.NonEmpty (nonEmpty)
 | 
			
		||||
import Data.Maybe (fromMaybe)
 | 
			
		||||
import Data.Ord (comparing)
 | 
			
		||||
import Data.Semigroup (sconcat)
 | 
			
		||||
import Data.Text (Text)
 | 
			
		||||
import qualified Data.Text as T
 | 
			
		||||
import Data.Time (Day)
 | 
			
		||||
import Safe
 | 
			
		||||
import Safe (headDef)
 | 
			
		||||
import System.Directory (doesFileExist, getHomeDirectory)
 | 
			
		||||
import System.Environment (getEnv)
 | 
			
		||||
import System.Exit (exitFailure)
 | 
			
		||||
import System.FilePath
 | 
			
		||||
import System.FilePath ((<.>), (</>), splitDirectories, splitFileName)
 | 
			
		||||
import System.Info (os)
 | 
			
		||||
import System.IO
 | 
			
		||||
import Text.Printf
 | 
			
		||||
import System.IO (stderr, writeFile)
 | 
			
		||||
import Text.Printf (hPrintf, printf)
 | 
			
		||||
 | 
			
		||||
import Hledger.Data.Dates (getCurrentDay, parsedate, showDate)
 | 
			
		||||
import Hledger.Data.Types
 | 
			
		||||
@ -150,11 +152,7 @@ type PrefixedFilePath = FilePath
 | 
			
		||||
-- Also the final parse state saved in the Journal does span all files.
 | 
			
		||||
readJournalFiles :: InputOpts -> [PrefixedFilePath] -> IO (Either String Journal)
 | 
			
		||||
readJournalFiles iopts =
 | 
			
		||||
  (right mconcat1 . sequence <$>) . mapM (readJournalFile iopts)
 | 
			
		||||
  where
 | 
			
		||||
    mconcat1 :: Monoid t => [t] -> t
 | 
			
		||||
    mconcat1 [] = mempty
 | 
			
		||||
    mconcat1 x  = foldr1 mappend x
 | 
			
		||||
  fmap (right (maybe def sconcat . nonEmpty) . sequence) . mapM (readJournalFile iopts)
 | 
			
		||||
 | 
			
		||||
-- | Read a Journal from this file, or from stdin if the file path is -,
 | 
			
		||||
-- or return an error message. The file path can have a READER: prefix.
 | 
			
		||||
 | 
			
		||||
@ -222,7 +222,7 @@ rtp = runTextParser
 | 
			
		||||
runJournalParser, rjp
 | 
			
		||||
  :: Monad m
 | 
			
		||||
  => JournalParser m a -> Text -> m (Either (ParseErrorBundle Text CustomErr) a)
 | 
			
		||||
runJournalParser p t = runParserT (evalStateT p mempty) "" t
 | 
			
		||||
runJournalParser p t = runParserT (evalStateT p nulljournal) "" t
 | 
			
		||||
rjp = runJournalParser
 | 
			
		||||
 | 
			
		||||
-- | Run an erroring journal parser in some monad. See also: parseWithState.
 | 
			
		||||
@ -232,7 +232,7 @@ runErroringJournalParser, rejp
 | 
			
		||||
  -> Text
 | 
			
		||||
  -> m (Either FinalParseError (Either (ParseErrorBundle Text CustomErr) a))
 | 
			
		||||
runErroringJournalParser p t =
 | 
			
		||||
  runExceptT $ runParserT (evalStateT p mempty) "" t
 | 
			
		||||
  runExceptT $ runParserT (evalStateT p nulljournal) "" t
 | 
			
		||||
rejp = runErroringJournalParser
 | 
			
		||||
 | 
			
		||||
genericSourcePos :: SourcePos -> GenericSourcePos
 | 
			
		||||
@ -680,7 +680,7 @@ amountwithoutpricep = do
 | 
			
		||||
-- | Parse an amount from a string, or get an error.
 | 
			
		||||
amountp' :: String -> Amount
 | 
			
		||||
amountp' s =
 | 
			
		||||
  case runParser (evalStateT (amountp <* eof) mempty) "" (T.pack s) of
 | 
			
		||||
  case runParser (evalStateT (amountp <* eof) nulljournal) "" (T.pack s) of
 | 
			
		||||
    Right amt -> amt
 | 
			
		||||
    Left err  -> error' $ show err -- XXX should throwError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -963,7 +963,7 @@ mkPosting rules record number accountFld amountFld amountInFld amountOutFld bala
 | 
			
		||||
          | all isSpace str = Nothing
 | 
			
		||||
          | otherwise = Just
 | 
			
		||||
              (either (balanceerror n str) id $
 | 
			
		||||
                runParser (evalStateT (amountp <* eof) mempty) "" $
 | 
			
		||||
                runParser (evalStateT (amountp <* eof) nulljournal) "" $
 | 
			
		||||
                T.pack $ (currency++) $ simplifySign str
 | 
			
		||||
              ,nullsourcepos)  -- XXX parse position to show when assertion fails,
 | 
			
		||||
                               -- the csv record's line number would be good
 | 
			
		||||
@ -1039,7 +1039,7 @@ chooseAmount rules record currency amountFld amountInFld amountOutFld =
 | 
			
		||||
 | 
			
		||||
   parseAmount currency amountstr =
 | 
			
		||||
     either (amounterror amountstr) (Mixed . (:[]))
 | 
			
		||||
     <$> runParser (evalStateT (amountp <* eof) mempty) ""
 | 
			
		||||
     <$> runParser (evalStateT (amountp <* eof) nulljournal) ""
 | 
			
		||||
     <$> T.pack
 | 
			
		||||
     <$> (currency++)
 | 
			
		||||
     <$> simplifySign
 | 
			
		||||
 | 
			
		||||
@ -85,7 +85,7 @@ import Control.Monad.Trans.Class (lift)
 | 
			
		||||
import Data.Either (isRight)
 | 
			
		||||
import qualified Data.Map.Strict as M
 | 
			
		||||
#if !(MIN_VERSION_base(4,11,0))
 | 
			
		||||
import Data.Monoid ((<>))
 | 
			
		||||
import Data.Semigroup ((<>))
 | 
			
		||||
#endif
 | 
			
		||||
import Data.Text (Text)
 | 
			
		||||
import Data.String
 | 
			
		||||
@ -298,7 +298,7 @@ includedirectivep = do
 | 
			
		||||
      put $ updatedChildj <> parentj
 | 
			
		||||
 | 
			
		||||
    newJournalWithParseStateFrom :: FilePath -> Journal -> Journal
 | 
			
		||||
    newJournalWithParseStateFrom filepath j = mempty{
 | 
			
		||||
    newJournalWithParseStateFrom filepath j = nulljournal{
 | 
			
		||||
      jparsedefaultyear      = jparsedefaultyear j
 | 
			
		||||
      ,jparsedefaultcommodity = jparsedefaultcommodity j
 | 
			
		||||
      ,jparseparentaccounts   = jparseparentaccounts j
 | 
			
		||||
@ -747,7 +747,7 @@ tests_JournalReader = tests "JournalReader" [
 | 
			
		||||
    ,test "yearless date with no default year" $ assertParseError datep "1/1" "current year is unknown"
 | 
			
		||||
    ,test "yearless date with default year" $ do
 | 
			
		||||
      let s = "1/1"
 | 
			
		||||
      ep <- parseWithState mempty{jparsedefaultyear=Just 2018} datep s
 | 
			
		||||
      ep <- parseWithState nulljournal{jparsedefaultyear=Just 2018} datep s
 | 
			
		||||
      either (assertFailure . ("parse error at "++) . customErrorBundlePretty) (const $ return ()) ep
 | 
			
		||||
    ,test "no leading zero" $ assertParse datep "2018/1/1"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -25,8 +25,9 @@ where
 | 
			
		||||
 | 
			
		||||
import Control.Monad.Except (ExceptT, runExceptT)
 | 
			
		||||
import Control.Monad.State.Strict (StateT, evalStateT, execStateT)
 | 
			
		||||
import Data.Default (Default(..))
 | 
			
		||||
#if !(MIN_VERSION_base(4,11,0))
 | 
			
		||||
import Data.Monoid ((<>))
 | 
			
		||||
import Data.Semigroup ((<>))
 | 
			
		||||
#endif
 | 
			
		||||
-- import Data.CallStack
 | 
			
		||||
import Data.List (isInfixOf)
 | 
			
		||||
@ -73,35 +74,35 @@ assertRight (Left a)  = assertFailure $ "expected Right, got (Left " ++ show a +
 | 
			
		||||
-- | Assert that this stateful parser runnable in IO successfully parses
 | 
			
		||||
-- all of the given input text, showing the parse error if it fails.
 | 
			
		||||
-- Suitable for hledger's JournalParser parsers.
 | 
			
		||||
assertParse :: (HasCallStack, Eq a, Show a, Monoid st) =>
 | 
			
		||||
assertParse :: (HasCallStack, Eq a, Show a, Default st) =>
 | 
			
		||||
  StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> Assertion
 | 
			
		||||
assertParse parser input = do
 | 
			
		||||
  ep <- runParserT (evalStateT (parser <* eof) mempty) "" input
 | 
			
		||||
  ep <- runParserT (evalStateT (parser <* eof) def) "" input
 | 
			
		||||
  either (assertFailure.(++"\n").("\nparse error at "++).customErrorBundlePretty)
 | 
			
		||||
         (const $ return ())
 | 
			
		||||
         ep
 | 
			
		||||
 | 
			
		||||
-- | Assert a parser produces an expected value.
 | 
			
		||||
assertParseEq :: (HasCallStack, Eq a, Show a, Monoid st) =>
 | 
			
		||||
assertParseEq :: (HasCallStack, Eq a, Show a, Default st) =>
 | 
			
		||||
  StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> Assertion
 | 
			
		||||
assertParseEq parser input expected = assertParseEqOn parser input id expected
 | 
			
		||||
 | 
			
		||||
-- | Like assertParseEq, but transform the parse result with the given function
 | 
			
		||||
-- before comparing it.
 | 
			
		||||
assertParseEqOn :: (HasCallStack, Eq b, Show b, Monoid st) =>
 | 
			
		||||
assertParseEqOn :: (HasCallStack, Eq b, Show b, Default st) =>
 | 
			
		||||
  StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> Assertion
 | 
			
		||||
assertParseEqOn parser input f expected = do
 | 
			
		||||
  ep <- runParserT (evalStateT (parser <* eof) mempty) "" input
 | 
			
		||||
  ep <- runParserT (evalStateT (parser <* eof) def) "" input
 | 
			
		||||
  either (assertFailure . (++"\n") . ("\nparse error at "++) . customErrorBundlePretty)
 | 
			
		||||
         (assertEqual "" expected . f)
 | 
			
		||||
         ep
 | 
			
		||||
 | 
			
		||||
-- | Assert that this stateful parser runnable in IO fails to parse
 | 
			
		||||
-- the given input text, with a parse error containing the given string.
 | 
			
		||||
assertParseError :: (HasCallStack, Eq a, Show a, Monoid st) =>
 | 
			
		||||
assertParseError :: (HasCallStack, Eq a, Show a, Default st) =>
 | 
			
		||||
  StateT st (ParsecT CustomErr T.Text IO) a -> String -> String -> Assertion
 | 
			
		||||
assertParseError parser input errstr = do
 | 
			
		||||
  ep <- runParserT (evalStateT parser mempty) "" (T.pack input)
 | 
			
		||||
  ep <- runParserT (evalStateT parser def) "" (T.pack input)
 | 
			
		||||
  case ep of
 | 
			
		||||
    Right v -> assertFailure $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n"
 | 
			
		||||
    Left e  -> do
 | 
			
		||||
@ -113,28 +114,28 @@ assertParseError parser input errstr = do
 | 
			
		||||
-- | Run a stateful parser in IO like assertParse, then assert that the
 | 
			
		||||
-- final state (the wrapped state, not megaparsec's internal state),
 | 
			
		||||
-- transformed by the given function, matches the given expected value.
 | 
			
		||||
assertParseStateOn :: (HasCallStack, Eq b, Show b, Monoid st) =>
 | 
			
		||||
assertParseStateOn :: (HasCallStack, Eq b, Show b, Default st) =>
 | 
			
		||||
     StateT st (ParsecT CustomErr T.Text IO) a
 | 
			
		||||
  -> T.Text
 | 
			
		||||
  -> (st -> b)
 | 
			
		||||
  -> b
 | 
			
		||||
  -> Assertion
 | 
			
		||||
assertParseStateOn parser input f expected = do
 | 
			
		||||
  es <- runParserT (execStateT (parser <* eof) mempty) "" input
 | 
			
		||||
  es <- runParserT (execStateT (parser <* eof) def) "" input
 | 
			
		||||
  case es of
 | 
			
		||||
    Left err -> assertFailure $ (++"\n") $ ("\nparse error at "++) $ customErrorBundlePretty err
 | 
			
		||||
    Right s  -> assertEqual "" expected $ f s
 | 
			
		||||
 | 
			
		||||
-- | These "E" variants of the above are suitable for hledger's ErroringJournalParser parsers.
 | 
			
		||||
assertParseE
 | 
			
		||||
  :: (HasCallStack, Eq a, Show a, Monoid st)
 | 
			
		||||
  :: (HasCallStack, Eq a, Show a, Default st)
 | 
			
		||||
  => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | 
			
		||||
  -> T.Text
 | 
			
		||||
  -> Assertion
 | 
			
		||||
assertParseE parser input = do
 | 
			
		||||
  let filepath = ""
 | 
			
		||||
  eep <- runExceptT $
 | 
			
		||||
           runParserT (evalStateT (parser <* eof) mempty) filepath input
 | 
			
		||||
           runParserT (evalStateT (parser <* eof) def) filepath input
 | 
			
		||||
  case eep of
 | 
			
		||||
    Left finalErr ->
 | 
			
		||||
      let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
 | 
			
		||||
@ -145,7 +146,7 @@ assertParseE parser input = do
 | 
			
		||||
             ep
 | 
			
		||||
 | 
			
		||||
assertParseEqE
 | 
			
		||||
  :: (Monoid st, Eq a, Show a, HasCallStack)
 | 
			
		||||
  :: (Default st, Eq a, Show a, HasCallStack)
 | 
			
		||||
  => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | 
			
		||||
  -> T.Text
 | 
			
		||||
  -> a
 | 
			
		||||
@ -153,7 +154,7 @@ assertParseEqE
 | 
			
		||||
assertParseEqE parser input expected = assertParseEqOnE parser input id expected
 | 
			
		||||
 | 
			
		||||
assertParseEqOnE
 | 
			
		||||
  :: (HasCallStack, Eq b, Show b, Monoid st)
 | 
			
		||||
  :: (HasCallStack, Eq b, Show b, Default st)
 | 
			
		||||
  => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | 
			
		||||
  -> T.Text
 | 
			
		||||
  -> (a -> b)
 | 
			
		||||
@ -161,7 +162,7 @@ assertParseEqOnE
 | 
			
		||||
  -> Assertion
 | 
			
		||||
assertParseEqOnE parser input f expected = do
 | 
			
		||||
  let filepath = ""
 | 
			
		||||
  eep <- runExceptT $ runParserT (evalStateT (parser <* eof) mempty) filepath input
 | 
			
		||||
  eep <- runExceptT $ runParserT (evalStateT (parser <* eof) def) filepath input
 | 
			
		||||
  case eep of
 | 
			
		||||
    Left finalErr ->
 | 
			
		||||
      let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
 | 
			
		||||
@ -172,14 +173,14 @@ assertParseEqOnE parser input f expected = do
 | 
			
		||||
             ep
 | 
			
		||||
 | 
			
		||||
assertParseErrorE
 | 
			
		||||
  :: (Monoid st, Eq a, Show a, HasCallStack)
 | 
			
		||||
  :: (Default st, Eq a, Show a, HasCallStack)
 | 
			
		||||
  => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
 | 
			
		||||
  -> T.Text
 | 
			
		||||
  -> String
 | 
			
		||||
  -> Assertion
 | 
			
		||||
assertParseErrorE parser input errstr = do
 | 
			
		||||
  let filepath = ""
 | 
			
		||||
  eep <- runExceptT $ runParserT (evalStateT parser mempty) filepath input
 | 
			
		||||
  eep <- runExceptT $ runParserT (evalStateT parser def) filepath input
 | 
			
		||||
  case eep of
 | 
			
		||||
    Left finalErr -> do
 | 
			
		||||
      let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
 | 
			
		||||
 | 
			
		||||
@ -166,7 +166,7 @@ validatePostings acctRes amtRes = let
 | 
			
		||||
                          foldl (\s a -> s <> parseErrorTextPretty a) "" .
 | 
			
		||||
                          bundleErrors)
 | 
			
		||||
  checkAccount = errorToFormMsg . runParser (accountnamep <* eof) "" . T.strip
 | 
			
		||||
  checkAmount = errorToFormMsg . runParser (evalStateT (amountp <* eof) mempty) "" . T.strip
 | 
			
		||||
  checkAmount = errorToFormMsg . runParser (evalStateT (amountp <* eof) nulljournal) "" . T.strip
 | 
			
		||||
 | 
			
		||||
  -- Add errors to forms with zero or one rows if the form is not a FormMissing
 | 
			
		||||
  result :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)]
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user