Slightly higher (with small files) and lower (with large files) maximum residency, and slightly quicker for all. hledger -f data/100x100x10.journal stats <<ghc: 42858472 bytes, 84 GCs, 193712/269608 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.015 elapsed), 0.016 MUT (0.042 elapsed), 0.011 GC (0.119 elapsed) :ghc>> <<ghc: 42891776 bytes, 84 GCs, 190816/260920 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.004 elapsed), 0.017 MUT (0.025 elapsed), 0.010 GC (0.015 elapsed) :ghc>> hledger -f data/1000x1000x10.journal stats <<ghc: 349575240 bytes, 681 GCs, 1396425/4091680 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.137 MUT (0.146 elapsed), 0.050 GC (0.057 elapsed) :ghc>> <<ghc: 349927568 bytes, 681 GCs, 1397825/4097248 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.126 MUT (0.133 elapsed), 0.050 GC (0.057 elapsed) :ghc>> hledger -f data/10000x1000x10.journal stats <<ghc: 3424029496 bytes, 6658 GCs, 11403141/41077288 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.000 elapsed), 1.278 MUT (1.310 elapsed), 0.493 GC (0.546 elapsed) :ghc>> <<ghc: 3427418064 bytes, 6665 GCs, 11127869/37790168 avg/max bytes residency (11 samples), 109M in use, 0.000 INIT (0.001 elapsed), 1.212 MUT (1.229 elapsed), 0.466 GC (0.519 elapsed) :ghc>> hledger -f data/100000x1000x10.journal stats <<ghc: 34306546248 bytes, 66727 GCs, 77030638/414617944 avg/max bytes residency (14 samples), 1012M in use, 0.000 INIT (0.000 elapsed), 12.965 MUT (13.164 elapsed), 4.771 GC (5.447 elapsed) :ghc>> <<ghc: 34340246056 bytes, 66779 GCs, 76983178/416011480 avg/max bytes residency (14 samples), 1011M in use, 0.000 INIT (0.008 elapsed), 12.666 MUT (12.836 elapsed), 4.595 GC (5.175 elapsed) :ghc>>
		
			
				
	
	
		
			125 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			125 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
{-# LANGUAGE CPP, FlexibleContexts, OverloadedStrings, QuasiQuotes, RecordWildCards #-}
 | 
						|
-- | Add form data & handler. (The layout and js are defined in
 | 
						|
-- Foundation so that the add form can be in the default layout for
 | 
						|
-- all views.)
 | 
						|
 | 
						|
module Handler.AddForm where
 | 
						|
 | 
						|
import Import
 | 
						|
 | 
						|
#if !MIN_VERSION_base(4,8,0)
 | 
						|
import Control.Applicative
 | 
						|
#endif
 | 
						|
import Data.Either (lefts,rights)
 | 
						|
import Data.List (sort)
 | 
						|
import qualified Data.List as L (head) -- qualified keeps dev & prod builds warning-free
 | 
						|
import Data.Text (append, pack, unpack)
 | 
						|
import qualified Data.Text as T
 | 
						|
import Data.Time.Calendar
 | 
						|
import Text.Parsec (digit, eof, many1, string, runParser)
 | 
						|
 | 
						|
import Hledger.Utils
 | 
						|
import Hledger.Data hiding (num)
 | 
						|
import Hledger.Read
 | 
						|
import Hledger.Cli hiding (num)
 | 
						|
 | 
						|
 | 
						|
-- Part of the data required from the add form.
 | 
						|
-- Don't know how to handle the variable posting fields with yesod-form yet.
 | 
						|
data AddForm = AddForm
 | 
						|
    { addFormDate         :: Day
 | 
						|
    , addFormDescription  :: Maybe Text -- String
 | 
						|
    -- , addFormPostings     :: [(AccountName, String)]
 | 
						|
    , addFormJournalFile  :: Maybe Text -- FilePath
 | 
						|
    }
 | 
						|
  deriving Show
 | 
						|
 | 
						|
postAddForm :: Handler Html
 | 
						|
postAddForm = do
 | 
						|
  let showErrors errs = do
 | 
						|
        -- error $ show errs -- XXX uncomment to prevent redirect for debugging
 | 
						|
        setMessage [shamlet|
 | 
						|
                    Errors:<br>
 | 
						|
                    $forall e<-errs
 | 
						|
                     \#{e}<br>
 | 
						|
                   |]
 | 
						|
                                
 | 
						|
  -- 1. process the fixed fields with yesod-form
 | 
						|
 | 
						|
  VD{..} <- getViewData
 | 
						|
  let
 | 
						|
      validateJournalFile :: Text -> Either FormMessage Text
 | 
						|
      validateJournalFile f
 | 
						|
        | unpack f `elem` journalFilePaths j = Right f
 | 
						|
        | otherwise                          = Left $ MsgInvalidEntry $ pack "the selected journal file \"" `append` f `append` "\"is unknown"
 | 
						|
 | 
						|
      validateDate :: Text -> Handler (Either FormMessage Day)
 | 
						|
      validateDate s = return $
 | 
						|
        case fixSmartDateStrEither' today $ strip $ unpack s of
 | 
						|
          Right d  -> Right d
 | 
						|
          Left _   -> Left $ MsgInvalidEntry $ pack "could not parse date \"" `append` s `append` pack "\":" -- ++ show e)
 | 
						|
 | 
						|
  formresult <- runInputPostResult $ AddForm
 | 
						|
    <$> ireq (checkMMap validateDate (pack . show) textField) "date"
 | 
						|
    <*> iopt textField "description"
 | 
						|
    <*> iopt (check validateJournalFile textField) "journal"
 | 
						|
  
 | 
						|
  ok <- case formresult of
 | 
						|
    FormMissing      -> showErrors ["there is no form data"::String] >> return False
 | 
						|
    FormFailure errs -> showErrors errs >> return False
 | 
						|
    FormSuccess dat  -> do
 | 
						|
      let AddForm{
 | 
						|
             addFormDate       =date
 | 
						|
            ,addFormDescription=mdesc
 | 
						|
            ,addFormJournalFile=mjournalfile
 | 
						|
            } = dat
 | 
						|
          desc = maybe "" unpack mdesc
 | 
						|
          journalfile = maybe (journalFilePath j) unpack mjournalfile
 | 
						|
 | 
						|
      -- 2. the fixed fields look good; now process the posting fields adhocly,
 | 
						|
      -- getting either errors or a balanced transaction
 | 
						|
 | 
						|
      (params,_) <- runRequestBody
 | 
						|
      let numberedParams s =
 | 
						|
            reverse $ dropWhile (T.null . snd) $ reverse $ sort
 | 
						|
            [ (n,v) | (k,v) <- params
 | 
						|
                    , let en = parsewith (paramnamep s) $ T.unpack k
 | 
						|
                    , isRight en
 | 
						|
                    , let Right n = en
 | 
						|
                    ]
 | 
						|
            where paramnamep s = do {string s; n <- many1 digit; eof; return (read n :: Int)}
 | 
						|
          acctparams = numberedParams "account"
 | 
						|
          amtparams  = numberedParams "amount"
 | 
						|
          num = length acctparams
 | 
						|
          paramErrs | num == 0 = ["at least one posting must be entered"]
 | 
						|
                    | map fst acctparams == [1..num] &&
 | 
						|
                      map fst amtparams `elem` [[1..num], [1..num-1]] = []
 | 
						|
                    | otherwise = ["the posting parameters are malformed"]
 | 
						|
          eaccts = map (runParser (accountnamep <* eof) () "" . textstrip  . snd) acctparams
 | 
						|
          eamts  = map (runParser (amountp <* eof) mempty "" . textstrip . snd) amtparams
 | 
						|
          (accts, acctErrs) = (rights eaccts, map show $ lefts eaccts)
 | 
						|
          (amts', amtErrs)  = (rights eamts, map show $ lefts eamts)
 | 
						|
          amts | length amts' == num = amts'
 | 
						|
               | otherwise           = amts' ++ [missingamt]
 | 
						|
          errs = if not (null paramErrs) then paramErrs else (acctErrs ++ amtErrs)
 | 
						|
          etxn | not $ null errs = Left errs
 | 
						|
               | otherwise = either (\e -> Left [L.head $ lines e]) Right
 | 
						|
                              (balanceTransaction Nothing $ nulltransaction {
 | 
						|
                                  tdate=date
 | 
						|
                                 ,tdescription=T.pack desc
 | 
						|
                                 ,tpostings=[nullposting{paccount=acct, pamount=Mixed [amt]} | (acct,amt) <- zip accts amts]
 | 
						|
                                 })
 | 
						|
      case etxn of
 | 
						|
       Left errs -> showErrors errs >> return False
 | 
						|
       Right t -> do
 | 
						|
        -- 3. all fields look good and form a balanced transaction; append it to the file
 | 
						|
        liftIO $ do ensureJournalFileExists journalfile
 | 
						|
                    appendToJournalFileOrStdout journalfile $
 | 
						|
                      showTransaction $
 | 
						|
                      txnTieKnot -- XXX move into balanceTransaction
 | 
						|
                      t
 | 
						|
        setMessage [shamlet|<span>Transaction added.|]
 | 
						|
        return True
 | 
						|
 | 
						|
  if ok then redirect JournalR else redirect (JournalR, [("add","1")])
 |