10% more allocation, but 35% lower maximum residency, and slightly quicker. hledger -f data/100x100x10.journal stats <<ghc: 39327768 bytes, 77 GCs, 196834/269496 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.010 elapsed), 0.020 MUT (0.092 elapsed), 0.014 GC (0.119 elapsed) :ghc>> <<ghc: 42842136 bytes, 84 GCs, 194010/270912 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.009 elapsed), 0.016 MUT (0.029 elapsed), 0.012 GC (0.120 elapsed) :ghc>> hledger -f data/1000x1000x10.journal stats <<ghc: 314291440 bytes, 612 GCs, 2070776/6628048 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.000 elapsed), 0.128 MUT (0.144 elapsed), 0.059 GC (0.070 elapsed) :ghc>> <<ghc: 349558872 bytes, 681 GCs, 1397597/4106384 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.004 elapsed), 0.124 MUT (0.133 elapsed), 0.047 GC (0.053 elapsed) :ghc>> hledger -f data/10000x1000x10.journal stats <<ghc: 3070026824 bytes, 5973 GCs, 12698030/62951784 avg/max bytes residency (10 samples), 124M in use, 0.000 INIT (0.002 elapsed), 1.268 MUT (1.354 elapsed), 0.514 GC (0.587 elapsed) :ghc>> <<ghc: 3424013128 bytes, 6658 GCs, 11405501/41071624 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.001 elapsed), 1.343 MUT (1.406 elapsed), 0.511 GC (0.573 elapsed) :ghc>> hledger -f data/100000x1000x10.journal stats <<ghc: 30753387392 bytes, 59811 GCs, 117615462/666703600 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.000 elapsed), 12.068 MUT (12.238 elapsed), 6.015 GC (7.190 elapsed) :ghc>> <<ghc: 34306530696 bytes, 66727 GCs, 76806196/414629312 avg/max bytes residency (14 samples), 1009M in use, 0.000 INIT (0.010 elapsed), 14.357 MUT (16.370 elapsed), 5.298 GC (6.534 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) () "" . T.pack . strip . T.unpack . snd) acctparams
 | |
|           eamts  = map (runParser (amountp <* eof) mempty "" . T.pack . strip . T.unpack . 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=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")])
 |