packages feed

hledger 0.9 → 0.10

raw patch · 45 files changed

+5139/−3766 lines, 45 filesdep −hack-handler-happstackdep −happstackdep −happstack-datadep ~hledger-libdep ~testpackPVP ok

version bump matches the API change (PVP)

Dependencies removed: hack-handler-happstack, happstack, happstack-data, happstack-server, happstack-state

Dependency ranges changed: hledger-lib, testpack

API changes (from Hackage documentation)

- Options: Begin :: String -> Opt
- Options: BinaryFilename :: Opt
- Options: Cleared :: Opt
- Options: CostBasis :: Opt
- Options: Debug :: Opt
- Options: DebugNoUI :: Opt
- Options: Depth :: String -> Opt
- Options: Display :: String -> Opt
- Options: Effective :: Opt
- Options: Empty :: Opt
- Options: End :: String -> Opt
- Options: File :: String -> Opt
- Options: Help :: Opt
- Options: MonthlyOpt :: Opt
- Options: NoNewAccts :: Opt
- Options: NoTotal :: Opt
- Options: Period :: String -> Opt
- Options: QuarterlyOpt :: Opt
- Options: Real :: Opt
- Options: SubTotal :: Opt
- Options: UnCleared :: Opt
- Options: Verbose :: Opt
- Options: Version :: Opt
- Options: WeeklyOpt :: Opt
- Options: YearlyOpt :: Opt
- Options: data Opt
- Options: dateSpanFromOpts :: Day -> [Opt] -> DateSpan
- Options: depthFromOpts :: [Opt] -> Maybe Int
- Options: displayExprFromOpts :: [Opt] -> Maybe String
- Options: fixOptDates :: [Opt] -> IO [Opt]
- Options: instance Eq Opt
- Options: instance Show Opt
- Options: intervalFromOpts :: [Opt] -> Interval
- Options: ledgerFilePathFromOpts :: [Opt] -> IO String
- Options: options :: [OptDescr Opt]
- Options: optsToFilterSpec :: [Opt] -> [String] -> LocalTime -> FilterSpec
- Options: parseArguments :: IO ([Opt], String, [String])
- Options: parsePatternArgs :: [String] -> ([String], [String])
- Options: usingTimeProgramName :: IO Bool
- Options: value :: Opt -> String
+ Hledger.Cli.Commands.Add: add :: [Opt] -> [String] -> Journal -> IO ()
+ Hledger.Cli.Commands.Add: appendToJournalFile :: Journal -> String -> IO ()
+ Hledger.Cli.Commands.Add: askFor :: String -> Maybe String -> Maybe (String -> Bool) -> IO String
+ Hledger.Cli.Commands.Add: compareDescriptions :: [Char] -> [Char] -> Double
+ Hledger.Cli.Commands.Add: compareStrings :: String -> String -> Double
+ Hledger.Cli.Commands.Add: getAndAddTransactions :: Journal -> [Opt] -> [String] -> Day -> IO ()
+ Hledger.Cli.Commands.Add: getPostings :: (AccountName -> Bool) -> Maybe [Posting] -> [Posting] -> IO [Posting]
+ Hledger.Cli.Commands.Add: getTransaction :: Journal -> [Opt] -> [String] -> Day -> IO (Transaction, Day)
+ Hledger.Cli.Commands.Add: journalAddTransaction :: Journal -> Transaction -> IO Journal
+ Hledger.Cli.Commands.Add: registerFromString :: String -> IO String
+ Hledger.Cli.Commands.Add: transactionsSimilarTo :: Journal -> [String] -> String -> [(Double, Transaction)]
+ Hledger.Cli.Commands.All: tests_Hledger_Commands :: Test
+ Hledger.Cli.Commands.Balance: balance :: [Opt] -> [String] -> Journal -> IO ()
+ Hledger.Cli.Commands.Balance: isInteresting :: [Opt] -> Ledger -> AccountName -> Bool
+ Hledger.Cli.Commands.Balance: showBalanceReport :: [Opt] -> FilterSpec -> Journal -> String
+ Hledger.Cli.Commands.Balance: showInterestingAccount :: Ledger -> [AccountName] -> AccountName -> String
+ Hledger.Cli.Commands.Convert: CsvRules :: Maybe FieldPosition -> Maybe FieldPosition -> Maybe FieldPosition -> Maybe FieldPosition -> Maybe FieldPosition -> Maybe FieldPosition -> Maybe String -> AccountName -> [AccountRule] -> CsvRules
+ Hledger.Cli.Commands.Convert: accountRules :: CsvRules -> [AccountRule]
+ Hledger.Cli.Commands.Convert: accountrule :: GenParser Char CsvRules AccountRule
+ Hledger.Cli.Commands.Convert: amountField :: CsvRules -> Maybe FieldPosition
+ Hledger.Cli.Commands.Convert: baseAccount :: CsvRules -> AccountName
+ Hledger.Cli.Commands.Convert: baseCurrency :: CsvRules -> Maybe String
+ Hledger.Cli.Commands.Convert: codeField :: CsvRules -> Maybe FieldPosition
+ Hledger.Cli.Commands.Convert: convert :: [Opt] -> [String] -> Journal -> IO ()
+ Hledger.Cli.Commands.Convert: csvrulesfile :: GenParser Char CsvRules CsvRules
+ Hledger.Cli.Commands.Convert: currencyField :: CsvRules -> Maybe FieldPosition
+ Hledger.Cli.Commands.Convert: data CsvRules
+ Hledger.Cli.Commands.Convert: dateField :: CsvRules -> Maybe FieldPosition
+ Hledger.Cli.Commands.Convert: definitions :: GenParser Char CsvRules ()
+ Hledger.Cli.Commands.Convert: descriptionField :: CsvRules -> Maybe FieldPosition
+ Hledger.Cli.Commands.Convert: identify :: [AccountRule] -> String -> String -> (String, String)
+ Hledger.Cli.Commands.Convert: initialRulesFileContent :: String
+ Hledger.Cli.Commands.Convert: instance Eq CsvRules
+ Hledger.Cli.Commands.Convert: instance Show CsvRules
+ Hledger.Cli.Commands.Convert: maxFieldIndex :: CsvRules -> Int
+ Hledger.Cli.Commands.Convert: normaliseDate :: String -> String
+ Hledger.Cli.Commands.Convert: parseCsvRules :: FilePath -> String -> Either ParseError CsvRules
+ Hledger.Cli.Commands.Convert: parseCsvRulesFile :: FilePath -> IO (Either ParseError CsvRules)
+ Hledger.Cli.Commands.Convert: printTxn :: Bool -> CsvRules -> CsvRecord -> IO ()
+ Hledger.Cli.Commands.Convert: rulesFileFor :: FilePath -> FilePath
+ Hledger.Cli.Commands.Convert: statusField :: CsvRules -> Maybe FieldPosition
+ Hledger.Cli.Commands.Convert: transactionFromCsvRecord :: CsvRules -> CsvRecord -> Transaction
+ Hledger.Cli.Commands.Convert: type AccountRule = ([(String, Maybe String)], AccountName)
+ Hledger.Cli.Commands.Convert: type CsvRecord = [String]
+ Hledger.Cli.Commands.Convert: type FieldPosition = Int
+ Hledger.Cli.Commands.Histogram: histogram :: [Opt] -> [String] -> Journal -> IO ()
+ Hledger.Cli.Commands.Histogram: showHistogram :: [Opt] -> FilterSpec -> Journal -> String
+ Hledger.Cli.Commands.Print: print' :: [Opt] -> [String] -> Journal -> IO ()
+ Hledger.Cli.Commands.Print: showTransactions :: FilterSpec -> Journal -> String
+ Hledger.Cli.Commands.Register: register :: [Opt] -> [String] -> Journal -> IO ()
+ Hledger.Cli.Commands.Register: showPostingWithBalance :: Bool -> Posting -> MixedAmount -> String
+ Hledger.Cli.Commands.Register: showRegisterReport :: [Opt] -> FilterSpec -> Journal -> String
+ Hledger.Cli.Commands.Register: tests_Register :: Test
+ Hledger.Cli.Commands.Stats: showStats :: [Opt] -> [String] -> Ledger -> Day -> String
+ Hledger.Cli.Commands.Stats: stats :: [Opt] -> [String] -> Journal -> IO ()
+ Hledger.Cli.Main: main :: IO ()
+ Hledger.Cli.Options: Begin :: String -> Opt
+ Hledger.Cli.Options: BinaryFilename :: Opt
+ Hledger.Cli.Options: Cleared :: Opt
+ Hledger.Cli.Options: CostBasis :: Opt
+ Hledger.Cli.Options: Debug :: Opt
+ Hledger.Cli.Options: DebugVty :: Opt
+ Hledger.Cli.Options: Depth :: String -> Opt
+ Hledger.Cli.Options: Display :: String -> Opt
+ Hledger.Cli.Options: Effective :: Opt
+ Hledger.Cli.Options: Empty :: Opt
+ Hledger.Cli.Options: End :: String -> Opt
+ Hledger.Cli.Options: File :: String -> Opt
+ Hledger.Cli.Options: Help :: Opt
+ Hledger.Cli.Options: MonthlyOpt :: Opt
+ Hledger.Cli.Options: NoNewAccts :: Opt
+ Hledger.Cli.Options: NoTotal :: Opt
+ Hledger.Cli.Options: Period :: String -> Opt
+ Hledger.Cli.Options: QuarterlyOpt :: Opt
+ Hledger.Cli.Options: Real :: Opt
+ Hledger.Cli.Options: SubTotal :: Opt
+ Hledger.Cli.Options: UnCleared :: Opt
+ Hledger.Cli.Options: Verbose :: Opt
+ Hledger.Cli.Options: Version :: Opt
+ Hledger.Cli.Options: WeeklyOpt :: Opt
+ Hledger.Cli.Options: YearlyOpt :: Opt
+ Hledger.Cli.Options: data Opt
+ Hledger.Cli.Options: dateSpanFromOpts :: Day -> [Opt] -> DateSpan
+ Hledger.Cli.Options: depthFromOpts :: [Opt] -> Maybe Int
+ Hledger.Cli.Options: displayExprFromOpts :: [Opt] -> Maybe String
+ Hledger.Cli.Options: fixOptDates :: [Opt] -> IO [Opt]
+ Hledger.Cli.Options: instance Eq Opt
+ Hledger.Cli.Options: instance Show Opt
+ Hledger.Cli.Options: intervalFromOpts :: [Opt] -> Interval
+ Hledger.Cli.Options: journalFilePathFromOpts :: [Opt] -> IO String
+ Hledger.Cli.Options: options :: [OptDescr Opt]
+ Hledger.Cli.Options: optsToFilterSpec :: [Opt] -> [String] -> LocalTime -> FilterSpec
+ Hledger.Cli.Options: parseArguments :: IO ([Opt], String, [String])
+ Hledger.Cli.Options: parsePatternArgs :: [String] -> ([String], [String])
+ Hledger.Cli.Options: usingTimeProgramName :: IO Bool
+ Hledger.Cli.Options: value :: Opt -> String
+ Hledger.Cli.Tests: journalWithAmounts :: [String] -> Journal
+ Hledger.Cli.Tests: runtests :: [Opt] -> [String] -> IO ()
+ Hledger.Cli.Tests: tests :: Test
+ Hledger.Cli.Utils: journalFromStringWithOpts :: [Opt] -> String -> IO Journal
+ Hledger.Cli.Utils: openBrowserOn :: String -> IO ExitCode
+ Hledger.Cli.Utils: withJournalDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Journal -> IO ()) -> IO ()

Files

+ CONTRIBUTORS.rst view
@@ -0,0 +1,71 @@+---+title: hledger Contributor List and Agreement+---+hledger Contributor List and Agreement+======================================++hledger Contributors+--------------------+hledger is brought to you by:++- Simon Michael - lead developer, project manager, general dogsbody+- Sergey Astanin - utf8 support+- Nick Ingolia - parser improvements+- Roman Cheplyaka - "chart" command, "add" command improvements++Developers who have not yet signed the contributor agreement:++- Tim Docker - parser improvements+- Marko Kocić - hlint cleanup+- Oliver Braun - GHC 6.12 compatibility+- Gwern Branwen - cleanups++Also:++- John Wiegley - created the original ledger program++.. raw:: html++ <br>++----++.. raw:: html++ <br>++hledger Contributor Agreement+-----------------------------+This is the official contributor agreement (v1) and contributor list+for the hledger project. If you have contributed non-trivial code+changes to hledger, please sign this, to help ensure:++1. that there is a clear legal status and audit trail for the project++2. that you get proper credit for your work++3. that we are able to remain license-compatible with related software+   by updating to newer versions of our license when appropriate+   (eg GPLv2 -> GPLv3)++To "sign", read the conditions below and then send a darcs patch to+`this file`_  adding your name to the Contributor List above.  By so+doing you promise that all of your commits to hledger:++- are free of patent violations or copyright violations, to the best of+  your knowledge++- are released under the hledger project's license_; or are released+  under another compatible license (in which case this must be clearly+  noted); or are public domain++- may be relicensed under official future versions of their license +  at the discretion of the project leader++We don't currently gather paper signatures, but this is a good start.+Feel free to update your entry as your contributions grow!+++.. _license:   http://joyful.com/repos/hledger/LICENSE+.. _this file: http://joyful.com//darcsweb/darcsweb.cgi?r=hledger;a=filehistory;f=/CONTRIBUTORS+
− Commands/Add.hs
@@ -1,202 +0,0 @@-{-# LANGUAGE CPP #-}-{-| --A history-aware add command to help with data entry.---}--module Commands.Add-where-import Ledger-import Options-import Commands.Register (showRegisterReport)-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding (putStr, putStrLn, getLine, appendFile)-import System.IO.UTF8-import System.IO ( stderr, hFlush )-#else-import System.IO ( stderr, hFlush, hPutStrLn, hPutStr )-#endif-import System.IO.Error-import Text.ParserCombinators.Parsec-import Utils (ledgerFromStringWithOpts)-import qualified Data.Foldable as Foldable (find)---- | Read ledger transactions from the terminal, prompting for each field,--- and append them to the ledger file. If the ledger came from stdin, this--- command has no effect.-add :: [Opt] -> [String] -> Ledger -> IO ()-add opts args l-    | filepath (journal l) == "-" = return ()-    | otherwise = do-  hPutStrLn stderr $-    "Enter one or more transactions, which will be added to your ledger file.\n"-    ++"To complete a transaction, enter . as account name. To quit, press control-c."-  today <- getCurrentDay-  getAndAddTransactions l opts args today `catch` (\e -> unless (isEOFError e) $ ioError e)---- | Read a number of ledger transactions from the command line,--- prompting, validating, displaying and appending them to the ledger--- file, until end of input (then raise an EOF exception). Any--- command-line arguments are used as the first transaction's description.-getAndAddTransactions :: Ledger -> [Opt] -> [String] -> Day -> IO ()-getAndAddTransactions l opts args defaultDate = do-  (ledgerTransaction,date) <- getTransaction l opts args defaultDate-  l <- ledgerAddTransaction l ledgerTransaction-  getAndAddTransactions l opts args date---- | Read a transaction from the command line, with history-aware prompting.-getTransaction :: Ledger -> [Opt] -> [String] -> Day -> IO (Transaction,Day)-getTransaction l opts args defaultDate = do-  today <- getCurrentDay-  datestr <- askFor "date" -            (Just $ showDate defaultDate)-            (Just $ \s -> null s || -             isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s))-  description <- askFor "description" Nothing (Just $ not . null) -  let historymatches = transactionsSimilarTo l args description-      bestmatch | null historymatches = Nothing-                | otherwise = Just $ snd $ head historymatches-      bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch-      date = fixSmartDate today $ fromparse $ (parse smartdate "" . lowercase) datestr-      accept x = x == "." || (not . null) x &&-        if NoNewAccts `elem` opts-            then isJust $ Foldable.find (== x) ant-            else True-        where (ant,_,_,_) = groupPostings . journalPostings . journal $ l-      getpostingsandvalidate = do-        ps <- getPostings accept bestmatchpostings []-        let t = nulltransaction{tdate=date-                               ,tstatus=False-                               ,tdescription=description-                               ,tpostings=ps-                               }-            retry msg = do-              hPutStrLn stderr $ "\n" ++ msg ++ "please re-enter."-              getpostingsandvalidate-        either retry (return . flip (,) date) $ balanceTransaction t-  unless (null historymatches) -       (do-         hPutStrLn stderr "Similar transactions found, using the first for defaults:\n"-         hPutStr stderr $ concatMap (\(n,t) -> printf "[%3d%%] %s" (round $ n*100 :: Int) (show t)) $ take 3 historymatches)-  getpostingsandvalidate---- | Read postings from the command line until . is entered, using the--- provided historical postings, if any, to guess defaults.-getPostings :: (AccountName -> Bool) -> Maybe [Posting] -> [Posting] -> IO [Posting]-getPostings accept historicalps enteredps = do-  account <- askFor (printf "account %d" n) defaultaccount (Just accept)-  if account=="."-    then return enteredps-    else do-      amountstr <- askFor (printf "amount  %d" n) defaultamount validateamount-      let amount = fromparse $ parse (someamount <|> return missingamt) "" amountstr-      let p = nullposting{paccount=stripbrackets account,-                          pamount=amount,-                          ptype=postingtype account}-      getPostings accept historicalps $ enteredps ++ [p]-    where-      n = length enteredps + 1-      enteredrealps = filter isReal enteredps-      bestmatch | isNothing historicalps = Nothing-                | n <= length ps = Just $ ps !! (n-1)-                | otherwise = Nothing-                where Just ps = historicalps-      defaultaccount = maybe Nothing (Just . showacctname) bestmatch-      showacctname p = showAccountName Nothing (ptype p) $ paccount p-      defaultamount = maybe balancingamount (Just . show . pamount) bestmatch-          where balancingamount = Just $ show $ negate $ sumMixedAmountsPreservingHighestPrecision $ map pamount enteredrealps-      postingtype ('[':_) = BalancedVirtualPosting-      postingtype ('(':_) = VirtualPosting-      postingtype _ = RegularPosting-      stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse-      validateamount = Just $ \s -> (null s && not (null enteredrealps))-                                   || isRight (parse (someamount>>many spacenonewline>>eof) "" s)---- | Prompt for and read a string value, optionally with a default value--- and a validator. A validator causes the prompt to repeat until the--- input is valid. May also raise an EOF exception if control-d is pressed.-askFor :: String -> Maybe String -> Maybe (String -> Bool) -> IO String-askFor prompt def validator = do-  hPutStr stderr $ prompt ++ maybe "" showdef def ++ ": "-  hFlush stderr-  l <- getLine-  let input = if null l then fromMaybe l def else l-  case validator of-    Just valid -> if valid input-                   then return input-                   else askFor prompt def validator-    Nothing -> return input-    where showdef s = " [" ++ s ++ "]"---- | Append this transaction to the ledger's file. Also, to the ledger's--- transaction list, but we don't bother updating the other fields - this--- is enough to include new transactions in the history matching.-ledgerAddTransaction :: Ledger -> Transaction -> IO Ledger-ledgerAddTransaction l t = do-  appendToLedgerFile l $ showTransaction t-  putStrLn $ printf "\nAdded transaction to %s:" (filepath $ journal l)-  putStrLn =<< registerFromString (show t)-  return l{journal=rl{jtxns=ts}}-      where rl = journal l-            ts = jtxns rl ++ [t]---- | Append data to the ledger's file, ensuring proper separation from any--- existing data; or if the file is "-", dump it to stdout.-appendToLedgerFile :: Ledger -> String -> IO ()-appendToLedgerFile l s = -    if f == "-"-    then putStr $ sep ++ s-    else appendFile f $ sep++s-    where -      f = filepath $ journal l-      -- XXX we are looking at the original raw text from when the ledger-      -- was first read, but that's good enough for now-      t = jtext $ journal l-      sep | null $ strip t = ""-          | otherwise = replicate (2 - min 2 (length lastnls)) '\n'-          where lastnls = takeWhile (=='\n') $ reverse t---- | Convert a string of ledger data into a register report.-registerFromString :: String -> IO String-registerFromString s = do-  now <- getCurrentLocalTime-  l <- ledgerFromStringWithOpts [] s-  return $ showRegisterReport opts (optsToFilterSpec opts [] now) l-    where opts = [Empty]---- | Return a similarity measure, from 0 to 1, for two strings.--- This is Simon White's letter pairs algorithm from--- http://www.catalysoft.com/articles/StrikeAMatch.html--- with a modification for short strings.-compareStrings :: String -> String -> Double-compareStrings "" "" = 1-compareStrings (_:[]) "" = 0-compareStrings "" (_:[]) = 0-compareStrings (a:[]) (b:[]) = if toUpper a == toUpper b then 1 else 0-compareStrings s1 s2 = 2.0 * fromIntegral i / fromIntegral u-    where-      i = length $ intersect pairs1 pairs2-      u = length pairs1 + length pairs2-      pairs1 = wordLetterPairs $ uppercase s1-      pairs2 = wordLetterPairs $ uppercase s2-wordLetterPairs = concatMap letterPairs . words-letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest)-letterPairs _ = []--compareLedgerDescriptions :: [Char] -> [Char] -> Double-compareLedgerDescriptions s t = compareStrings s' t'-    where s' = simplify s-          t' = simplify t-          simplify = filter (not . (`elem` "0123456789"))--transactionsSimilarTo :: Ledger -> [String] -> String -> [(Double,Transaction)]-transactionsSimilarTo l apats s =-    sortBy compareRelevanceAndRecency-               $ filter ((> threshold).fst)-               [(compareLedgerDescriptions s $ tdescription t, t) | t <- ts]-    where-      compareRelevanceAndRecency (n1,t1) (n2,t2) = compare (n2,tdate t2) (n1,tdate t1)-      ts = jtxns $ filterJournalTransactionsByAccount apats $ journal l-      threshold = 0-
− Commands/All.hs
@@ -1,67 +0,0 @@-{-# LANGUAGE CPP #-}-{-| --The Commands package defines all the commands offered by the hledger-application, like \"register\" and \"balance\".  This module exports all-the commands; you can also import individual modules if you prefer.---}--module Commands.All (-                     module Commands.Add,-                     module Commands.Balance,-                     module Commands.Convert,-                     module Commands.Histogram,-                     module Commands.Print,-                     module Commands.Register,-                     module Commands.Stats,-#ifdef VTY-                     module Commands.UI,-#endif-#if defined(WEB) || defined(WEBHAPPSTACK)-                     module Commands.Web,-#endif-#ifdef CHART-                     module Commands.Chart,-#endif-                     tests_Commands-              )-where-import Commands.Add-import Commands.Balance-import Commands.Convert-import Commands.Histogram-import Commands.Print-import Commands.Register-import Commands.Stats-#ifdef VTY-import Commands.UI-#endif-#if defined(WEB) || defined(WEBHAPPSTACK)-import Commands.Web-#endif-#ifdef CHART-import Commands.Chart-#endif-import Test.HUnit (Test(TestList))---tests_Commands = TestList-    [---      Commands.Add.tests_Add---     ,Commands.Balance.tests_Balance-     Commands.Convert.tests_Convert---     ,Commands.Histogram.tests_Histogram---     ,Commands.Print.tests_Print-    ,Commands.Register.tests_Register---     ,Commands.Stats.tests_Stats-    ]--- #ifdef VTY---     ,Commands.UI.tests_UI--- #endif--- #if defined(WEB) || defined(WEBHAPPSTACK)---     ,Commands.Web.tests_Web--- #endif--- #ifdef CHART---     ,Commands.Chart.tests_Chart--- #endif
− Commands/Balance.hs
@@ -1,165 +0,0 @@-{-# LANGUAGE CPP #-}-{-|--A ledger-compatible @balance@ command.--ledger's balance command is easy to use but not easy to describe-precisely.  In the examples below we'll use sample.ledger, which has the-following account tree:--@- assets-   bank-     checking-     saving-   cash- expenses-   food-   supplies- income-   gifts-   salary- liabilities-   debts-@--The balance command shows accounts with their aggregate balances.-Subaccounts are displayed indented below their parent. Each balance is the-sum of any transactions in that account plus any balances from-subaccounts:--@- $ hledger -f sample.ledger balance-                 $-1  assets-                  $1    bank:saving-                 $-2    cash-                  $2  expenses-                  $1    food-                  $1    supplies-                 $-2  income-                 $-1    gifts-                 $-1    salary-                  $1  liabilities:debts-@--Usually, the non-interesting accounts are elided or omitted. Above,-@checking@ is omitted because it has no subaccounts and a zero balance.-@bank@ is elided because it has only a single displayed subaccount-(@saving@) and it would be showing the same balance as that ($1). Ditto-for @liabilities@. We will return to this in a moment.--The --depth argument can be used to limit the depth of the balance report.-So, to see just the top level accounts:--@-$ hledger -f sample.ledger balance --depth 1-                 $-1  assets-                  $2  expenses-                 $-2  income-                  $1  liabilities-@--This time liabilities has no displayed subaccounts (due to --depth) and-is not elided.--With one or more account pattern arguments, the balance command shows-accounts whose name matches one of the patterns, plus their parents-(elided) and subaccounts. So with the pattern o we get:--@- $ hledger -f sample.ledger balance o-                  $1  expenses:food-                 $-2  income-                 $-1    gifts-                 $-1    salary----------------------                 $-1-@--The o pattern matched @food@ and @income@, so they are shown. Unmatched-parents of matched accounts are also shown (elided) for context (@expenses@).--Also, the balance report shows the total of all displayed accounts, when-that is non-zero. Here, it is displayed because the accounts shown add up-to $-1.--Here is a more precise definition of \"interesting\" accounts in ledger's-balance report:--- an account which has just one interesting subaccount branch, and which-  is not at the report's maximum depth, is interesting if the balance is-  different from the subaccount's, and otherwise boring.--- any other account is interesting if it has a non-zero balance, or the -E-  flag is used.---}--module Commands.Balance-where-import Ledger.Utils-import Ledger.Types-import Ledger.Amount-import Ledger.AccountName-import Ledger.Posting-import Ledger.Ledger-import Options-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding ( putStr )-import System.IO.UTF8-#endif----- | Print a balance report.-balance :: [Opt] -> [String] -> Ledger -> IO ()-balance opts args l = do-  t <- getCurrentLocalTime-  putStr $ showBalanceReport opts (optsToFilterSpec opts args t) l---- | Generate a balance report with the specified options for this ledger.-showBalanceReport :: [Opt] -> FilterSpec -> Ledger -> String-showBalanceReport opts filterspec l = acctsstr ++ totalstr-    where-      l' = cacheLedger'' filterspec l-      acctsstr = unlines $ map showacct interestingaccts-          where-            showacct = showInterestingAccount l' interestingaccts-            interestingaccts = filter (isInteresting opts l') acctnames-            acctnames = sort $ tail $ flatten $ treemap aname accttree-            accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l'-      totalstr | NoTotal `elem` opts = ""-               | notElem Empty opts && isZeroMixedAmount total = ""-               | otherwise = printf "--------------------\n%s\n" $ padleft 20 $ showMixedAmountWithoutPrice total-          where-            total = sum $ map abalance $ ledgerTopAccounts l'---- | Display one line of the balance report with appropriate indenting and eliding.-showInterestingAccount :: Ledger -> [AccountName] -> AccountName -> String-showInterestingAccount l interestingaccts a = concatTopPadded [amt, "  ", depthspacer ++ partialname]-    where-      amt = padleft 20 $ showMixedAmountWithoutPrice $ abalance $ ledgerAccount l a-      parents = parentAccountNames a-      interestingparents = filter (`elem` interestingaccts) parents-      depthspacer = replicate (indentperlevel * length interestingparents) ' '-      indentperlevel = 2-      -- the partial name is the account's leaf name, prefixed by the-      -- names of any boring parents immediately above-      partialname = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a]-          where ps = takeWhile boring parents where boring = not . (`elem` interestingparents)---- | Is the named account considered interesting for this ledger's balance report ?-isInteresting :: [Opt] -> Ledger -> AccountName -> Bool-isInteresting opts l a-    | numinterestingsubs==1 && not atmaxdepth = notlikesub-    | otherwise = notzero || emptyflag-    where-      atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depthFromOpts opts-      emptyflag = Empty `elem` opts-      acct = ledgerAccount l a-      notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct-      notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumPostings $ apostings acct-      numinterestingsubs = length $ filter isInterestingTree subtrees-          where-            isInterestingTree = treeany (isInteresting opts l . aname)-            subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a-
− Commands/Chart.hs
@@ -1,108 +0,0 @@-{-|--Generate balances pie chart---}--module Commands.Chart-where-import Ledger.Utils-import Ledger.Types-import Ledger.Amount-import Ledger.Ledger-import Ledger.Commodity-import Options--import Control.Monad (liftM3)-import Graphics.Rendering.Chart-import Data.Colour-import Data.Colour.Names-import Data.Colour.RGBSpace-import Data.Colour.RGBSpace.HSL (hsl)-import Data.Colour.SRGB.Linear (rgb)-import Data.List-import Safe (readDef)---- | Generate an image with the pie chart and write it to a file-chart :: [Opt] -> [String] -> Ledger -> IO ()-chart opts args l = do-  t <- getCurrentLocalTime-  let chart = genPie opts (optsToFilterSpec opts args t) l-  renderableToPNGFile (toRenderable chart) w h filename-    where-      filename = getOption opts ChartOutput chartoutput-      (w,h) = parseSize $ getOption opts ChartSize chartsize---- | Extract string option value from a list of options or use the default-getOption :: [Opt] -> (String->Opt) -> String -> String-getOption opts opt def = -    case reverse $ optValuesForConstructor opt opts of-        [] -> def-        x:_ -> x---- | Parse image size from a command-line option-parseSize :: String -> (Int,Int)-parseSize str = (read w, read h)-    where-    x = fromMaybe (error "Size should be in WIDTHxHEIGHT format") $ findIndex (=='x') str-    (w,_:h) = splitAt x str---- | Generate pie chart-genPie :: [Opt] -> FilterSpec -> Ledger -> PieLayout-genPie opts filterspec l = defaultPieLayout { pie_background_ = solidFillStyle $ opaque $ white-                                            , pie_plot_ = pie_chart }-    where-      pie_chart = defaultPieChart { pie_data_ = map (uncurry accountPieItem) chartitems'-                                  , pie_start_angle_ = (-90)-                                  , pie_colors_ = mkColours hue-                                  , pie_label_style_ = defaultFontStyle{font_size_=12}-                                  }-      chartitems' = debug "chart" $ top num samesignitems-      (samesignitems, sign) = sameSignNonZero rawitems-      rawitems = debug "raw" $ flatten $ balances $-                 ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) $ cacheLedger'' filterspec l-      top n t = topn ++ [other]-          where-            (topn,rest) = splitAt n $ reverse $ sortBy (comparing snd) t-            other = ("other", sum $ map snd rest)-      num = readDef (fromIntegral chartitems) (getOption opts ChartItems (show chartitems))-      hue = if sign > 0 then red else green where (red, green) = (0, 110)-      debug s = if Debug `elem` opts then ltrace s else id---- | Select the nonzero items with same sign as the first, and make--- them positive. Also return a 1 or -1 corresponding to the original sign.-sameSignNonZero :: [(AccountName, Double)] -> ([(AccountName, Double)], Int)-sameSignNonZero is | null nzs = ([], 1)-                   | otherwise = (map pos $ filter (test.snd) nzs, sign)-                   where-                     nzs = filter ((/=0).snd) is-                     pos (a,b) = (a, abs b)-                     sign = if snd (head nzs) >= 0 then 1 else (-1)-                     test = if sign > 0 then (>0) else (<0)---- | Convert all quantities of MixedAccount to a single commodity-amountValue :: MixedAmount -> Double-amountValue = quantity . convertMixedAmountTo unknown---- | Generate a tree of account names together with their balances.---   The balance of account is decremented by the balance of its subaccounts---   which are drawn on the chart.-balances :: Tree Account -> Tree (AccountName, Double)-balances (Node rootAcc subAccs) = Node newroot newsubs-    where-      newroot = (aname rootAcc,-                 amountValue $-                 abalance rootAcc - (sum . map (abalance . root)) subAccs)-      newsubs = map balances subAccs---- | Build a single pie chart item-accountPieItem :: AccountName -> Double -> PieItem-accountPieItem accname balance = PieItem accname offset balance where offset = 0---- | Generate an infinite color list suitable for charts.-mkColours :: Double -> [AlphaColour Double]-mkColours hue = cycle $ [opaque $ rgbToColour $ hsl h s l | (h,s,l) <- liftM3 (,,)-                         [hue] [0.7] [0.1,0.2..0.7] ]--rgbToColour :: (Fractional a) => RGB a -> Colour a-rgbToColour (RGB r g b) = rgb r g b
− Commands/Convert.hs
@@ -1,386 +0,0 @@-{-|-Convert account data in CSV format (eg downloaded from a bank) to ledger-format, and print it on stdout. See the manual for more details.--}--module Commands.Convert where-import Options (Opt(Debug))-import Version (versionstr)-import Ledger.Types (Ledger,AccountName,Transaction(..),Posting(..),PostingType(..))-import Ledger.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual)-import Ledger.Parse (someamount, emptyCtx, ledgeraccountname)-import Ledger.Amount (nullmixedamt)-import Safe (atDef, maximumDef)-import System.IO (stderr)-import Text.CSV (parseCSVFromFile, printCSV)-import Text.Printf (hPrintf)-import Text.RegexPR (matchRegexPR, gsubRegexPR)-import Data.Maybe-import Ledger.Dates (firstJust, showDate, parsedate)-import System.Locale (defaultTimeLocale)-import Data.Time.Format (parseTime)-import Control.Monad (when, guard, liftM)-import Safe (readDef, readMay)-import System.Directory (doesFileExist)-import System.Exit (exitFailure)-import System.FilePath.Posix (takeBaseName, replaceExtension)-import Text.ParserCombinators.Parsec-import Test.HUnit---{- |-A set of data definitions and account-matching patterns sufficient to-convert a particular CSV data file into meaningful ledger transactions. See above.--}-data CsvRules = CsvRules {-      dateField :: Maybe FieldPosition,-      statusField :: Maybe FieldPosition,-      codeField :: Maybe FieldPosition,-      descriptionField :: Maybe FieldPosition,-      amountField :: Maybe FieldPosition,-      currencyField :: Maybe FieldPosition,-      baseCurrency :: Maybe String,-      baseAccount :: AccountName,-      accountRules :: [AccountRule]-} deriving (Show, Eq)--nullrules = CsvRules {-      dateField=Nothing,-      statusField=Nothing,-      codeField=Nothing,-      descriptionField=Nothing,-      amountField=Nothing,-      currencyField=Nothing,-      baseCurrency=Nothing,-      baseAccount="unknown",-      accountRules=[]-}--type FieldPosition = Int--type AccountRule = (-   [(String, Maybe String)] -- list of regex match patterns with optional replacements-  ,AccountName              -- account name to use for a transaction matching this rule-  )--type CsvRecord = [String]----- | Read the CSV file named as an argument and print equivalent ledger transactions,--- using/creating a .rules file.-convert :: [Opt] -> [String] -> Ledger -> IO ()-convert opts args _ = do-  when (null args) $ error "please specify a csv data file."-  let csvfile = head args-  csvparse <- parseCSVFromFile csvfile-  let records = case csvparse of-                  Left e -> error $ show e-                  Right rs -> reverse $ filter (/= [""]) rs-  let debug = Debug `elem` opts-      rulesfile = rulesFileFor csvfile-  exists <- doesFileExist rulesfile-  if (not exists) then do-                  hPrintf stderr "creating conversion rules file %s, edit this file for better results\n" rulesfile-                  writeFile rulesfile initialRulesFileContent-   else-      hPrintf stderr "using conversion rules file %s\n" rulesfile-  rules <- liftM (either (error.show) id) $ parseCsvRulesFile rulesfile-  when debug $ hPrintf stderr "rules: %s\n" (show rules)-  let requiredfields = max 2 (maxFieldIndex rules + 1)-      badrecords = take 1 $ filter ((< requiredfields).length) records-  if null badrecords-   then mapM_ (printTxn debug rules) records-   else do-     hPrintf stderr (unlines [-                      "Warning, at least one CSV record does not contain a field referenced by the"-                     ,"conversion rules file, or has less than two fields. Are you converting a"-                     ,"valid CSV file ? First bad record:\n%s"-                     ]) (show $ head badrecords)-     exitFailure---- | The highest (0-based) field index referenced in the field--- definitions, or -1 if no fields are defined.-maxFieldIndex :: CsvRules -> Int-maxFieldIndex r = maximumDef (-1) $ catMaybes [-                   dateField r-                  ,statusField r-                  ,codeField r-                  ,descriptionField r-                  ,amountField r-                  ,currencyField r-                  ]--rulesFileFor :: FilePath -> FilePath-rulesFileFor csvfile = replaceExtension csvfile ".rules"--initialRulesFileContent :: String-initialRulesFileContent =-    "# csv conversion rules file generated by hledger "++versionstr++"\n" ++-    "# Add rules to this file for more accurate conversion, see\n"++-    "# http://hledger.org/MANUAL.html#convert\n" ++-    "\n" ++-    "base-account assets:bank:checking\n" ++-    "date-field 0\n" ++-    "description-field 4\n" ++-    "amount-field 1\n" ++-    "currency $\n" ++-    "\n" ++-    "# account-assigning rules\n" ++-    "\n" ++-    "SPECTRUM\n" ++-    "expenses:health:gym\n" ++-    "\n" ++-    "ITUNES\n" ++-    "BLKBSTR=BLOCKBUSTER\n" ++-    "expenses:entertainment\n" ++-    "\n" ++-    "(TO|FROM) SAVINGS\n" ++-    "assets:bank:savings\n"---- rules file parser--parseCsvRulesFile :: FilePath -> IO (Either ParseError CsvRules)-parseCsvRulesFile f = do-  s <- readFile f-  return $ parseCsvRules f s--parseCsvRules :: FilePath -> String -> Either ParseError CsvRules-parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s--csvrulesfile :: GenParser Char CsvRules CsvRules-csvrulesfile = do-  many blankorcommentline-  many definitions-  r <- getState-  ars <- many accountrule-  many blankorcommentline-  eof-  return r{accountRules=ars}---- | Real independent parser choice, even when alternative matches share a prefix.-choice' parsers = choice $ map try (init parsers) ++ [last parsers]--definitions :: GenParser Char CsvRules ()-definitions = do-  choice' [-    datefield-   ,statusfield-   ,codefield-   ,descriptionfield-   ,amountfield-   ,currencyfield-   ,basecurrency-   ,baseaccount-   ,commentline-   ] <?> "definition"-  return ()--datefield = do-  string "date-field"-  many1 spacenonewline-  v <- restofline-  r <- getState-  setState r{dateField=readMay v}--codefield = do-  string "code-field"-  many1 spacenonewline-  v <- restofline-  r <- getState-  setState r{codeField=readMay v}--statusfield = do-  string "status-field"-  many1 spacenonewline-  v <- restofline-  r <- getState-  setState r{statusField=readMay v}--descriptionfield = do-  string "description-field"-  many1 spacenonewline-  v <- restofline-  r <- getState-  setState r{descriptionField=readMay v}--amountfield = do-  string "amount-field"-  many1 spacenonewline-  v <- restofline-  r <- getState-  setState r{amountField=readMay v}--currencyfield = do-  string "currency-field"-  many1 spacenonewline-  v <- restofline-  r <- getState-  setState r{currencyField=readMay v}--basecurrency = do-  string "currency"-  many1 spacenonewline-  v <- restofline-  r <- getState-  setState r{baseCurrency=Just v}--baseaccount = do-  string "base-account"-  many1 spacenonewline-  v <- ledgeraccountname-  optional newline-  r <- getState-  setState r{baseAccount=v}--accountrule :: GenParser Char CsvRules AccountRule-accountrule = do-  many blankorcommentline-  pats <- many1 matchreplacepattern-  guard $ length pats >= 2-  let pats' = init pats-      acct = either (fail.show) id $ runParser ledgeraccountname () "" $ fst $ last pats-  many blankorcommentline-  return (pats',acct)- <?> "account rule"--blanklines = many1 blankline >> return ()--blankline = many spacenonewline >> newline >> return () <?> "blank line"--commentchar = oneOf ";#"--commentline = many spacenonewline >> commentchar >> restofline >> return () <?> "comment line"--blankorcommentline = choice' [blankline, commentline]--matchreplacepattern = do-  notFollowedBy commentchar-  matchpat <- many1 (noneOf "=\n")-  replpat <- optionMaybe $ do {char '='; many $ noneOf "\n"}-  newline-  return (matchpat,replpat)--printTxn :: Bool -> CsvRules -> CsvRecord -> IO ()-printTxn debug rules rec = do-  when debug $ hPrintf stderr "record: %s" (printCSV [rec])-  putStr $ show $ transactionFromCsvRecord rules rec---- csv record conversion--transactionFromCsvRecord :: CsvRules -> CsvRecord -> Transaction-transactionFromCsvRecord rules fields =-  let -      date = parsedate $ normaliseDate $ maybe "1900/1/1" (atDef "" fields) (dateField rules)-      status = maybe False (null . strip . (atDef "" fields)) (statusField rules)-      code = maybe "" (atDef "" fields) (codeField rules)-      desc = maybe "" (atDef "" fields) (descriptionField rules)-      comment = ""-      precomment = ""-      amountstr = maybe "" (atDef "" fields) (amountField rules)-      amountstr' = strnegate amountstr where strnegate ('-':s) = s-                                             strnegate s = '-':s-      currency = maybe (fromMaybe "" $ baseCurrency rules) (atDef "" fields) (currencyField rules)-      amountstr'' = currency ++ amountstr'-      amountparse = runParser someamount emptyCtx "" amountstr''-      amount = either (const nullmixedamt) id amountparse-      unknownacct | (readDef 0 amountstr' :: Double) < 0 = "income:unknown"-                  | otherwise = "expenses:unknown"-      (acct,newdesc) = identify (accountRules rules) unknownacct desc-      t = Transaction {-              tdate=date,-              teffectivedate=Nothing,-              tstatus=status,-              tcode=code,-              tdescription=newdesc,-              tcomment=comment,-              tpreceding_comment_lines=precomment,-              tpostings=[-                   Posting {-                     pstatus=False,-                     paccount=acct,-                     pamount=amount,-                     pcomment="",-                     ptype=RegularPosting,-                     ptransaction=Just t-                   },-                   Posting {-                     pstatus=False,-                     paccount=baseAccount rules,-                     pamount=(-amount),-                     pcomment="",-                     ptype=RegularPosting,-                     ptransaction=Just t-                   }-                  ]-            }-  in t---- | Convert some date string with unknown format to YYYY/MM/DD.-normaliseDate :: String -> String-normaliseDate s = maybe "0000/00/00" showDate $-              firstJust-              [parseTime defaultTimeLocale "%Y/%m/%e" s-               -- can't parse a month without leading 0, try adding one-              ,parseTime defaultTimeLocale "%Y/%m/%e" (take 5 s ++ "0" ++ drop 5 s)-              ,parseTime defaultTimeLocale "%Y-%m-%e" s-              ,parseTime defaultTimeLocale "%Y-%m-%e" (take 5 s ++ "0" ++ drop 5 s)-              ,parseTime defaultTimeLocale "%m/%e/%Y" s-              ,parseTime defaultTimeLocale "%m/%e/%Y" ('0':s)-              ,parseTime defaultTimeLocale "%m-%e-%Y" s-              ,parseTime defaultTimeLocale "%m-%e-%Y" ('0':s)-              ]---- | Apply account matching rules to a transaction description to obtain--- the most appropriate account and a new description.-identify :: [AccountRule] -> String -> String -> (String,String)-identify rules defacct desc | null matchingrules = (defacct,desc)-                            | otherwise = (acct,newdesc)-    where-      matchingrules = filter ismatch rules :: [AccountRule]-          where ismatch = any (isJust . flip matchRegexPR (caseinsensitive desc) . fst) . fst-      (prs,acct) = head matchingrules-      p_ms_r = filter (\(_,m,_) -> isJust m) $ map (\(p,r) -> (p, matchRegexPR (caseinsensitive p) desc, r)) prs-      (p,_,r) = head p_ms_r-      newdesc = case r of Just rpat -> gsubRegexPR (caseinsensitive p) rpat desc-                          Nothing   -> desc--caseinsensitive = ("(?i)"++)--tests_Convert = TestList [--   "convert rules parsing: empty file" ~: do-     -- let assertMixedAmountParse parseresult mixedamount =-     --         (either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)-    assertParseEqual (parseCsvRules "unknown" "") nullrules--  ,"convert rules parsing: accountrule" ~: do-     assertParseEqual (parseWithCtx nullrules accountrule "A\na\n") -- leading blank line required-                 ([("A",Nothing)], "a")--  ,"convert rules parsing: trailing comments" ~: do-     assertParse (parseWithCtx nullrules csvrulesfile "A\na\n# \n#\n")--  ,"convert rules parsing: trailing blank lines" ~: do-     assertParse (parseWithCtx nullrules csvrulesfile "A\na\n\n  \n")--  -- not supported-  -- ,"convert rules parsing: no final newline" ~: do-  --    assertParse (parseWithCtx nullrules csvrulesfile "A\na")-  --    assertParse (parseWithCtx nullrules csvrulesfile "A\na\n# \n#")-  --    assertParse (parseWithCtx nullrules csvrulesfile "A\na\n\n  ")--                 -- (nullrules{-                 --   -- dateField=Maybe FieldPosition,-                 --   -- statusField=Maybe FieldPosition,-                 --   -- codeField=Maybe FieldPosition,-                 --   -- descriptionField=Maybe FieldPosition,-                 --   -- amountField=Maybe FieldPosition,-                 --   -- currencyField=Maybe FieldPosition,-                 --   -- baseCurrency=Maybe String,-                 --   -- baseAccount=AccountName,-                 --   accountRules=[-                 --        ([("A",Nothing)], "a")-                 --       ]-                 --  })--  ]
− Commands/Histogram.hs
@@ -1,50 +0,0 @@-{-# LANGUAGE CPP #-}-{-| --Print a histogram report.---}--module Commands.Histogram-where-import Ledger-import Options-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding ( putStr )-import System.IO.UTF8-#endif---barchar = '*'---- | Print a histogram of some statistic per reporting interval, such as--- number of postings per day.-histogram :: [Opt] -> [String] -> Ledger -> IO ()-histogram opts args l = do-  t <- getCurrentLocalTime-  putStr $ showHistogram opts (optsToFilterSpec opts args t) l--showHistogram :: [Opt] -> FilterSpec -> Ledger -> String-showHistogram opts filterspec l = concatMap (printDayWith countBar) dayps-    where-      i = intervalFromOpts opts-      interval | i == NoInterval = Daily-               | otherwise = i-      fullspan = journalDateSpan $ journal l-      days = filter (DateSpan Nothing Nothing /=) $ splitSpan interval fullspan-      dayps = [(s, filter (isPostingInDateSpan s) ps) | s <- days]-      -- same as Register-      -- should count transactions, not postings ?-      ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ ledgerPostings l-      filterempties-          | Empty `elem` opts = id-          | otherwise = filter (not . isZeroMixedAmount . pamount)-      matchapats = matchpats apats . paccount-      apats = acctpats filterspec-      filterdepth | interval == NoInterval = filter (\p -> accountNameLevel (paccount p) <= depth)-                  | otherwise = id-      depth = fromMaybe 99999 $ depthFromOpts opts--printDayWith f (DateSpan b _, ts) = printf "%s %s\n" (show $ fromJust b) (f ts)--countBar ps = replicate (length ps) barchar
− Commands/Print.hs
@@ -1,29 +0,0 @@-{-# LANGUAGE CPP #-}-{-| --A ledger-compatible @print@ command.---}--module Commands.Print-where-import Ledger-import Options-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding ( putStr )-import System.IO.UTF8-#endif----- | Print ledger transactions in standard format.-print' :: [Opt] -> [String] -> Ledger -> IO ()-print' opts args l = do-  t <- getCurrentLocalTime-  putStr $ showTransactions (optsToFilterSpec opts args t) l--showTransactions :: FilterSpec -> Ledger -> String-showTransactions filterspec l =-    concatMap (showTransactionForPrint effective) $ sortBy (comparing tdate) txns-        where-          effective = EffectiveDate == whichdate filterspec-          txns = jtxns $ filterJournalTransactions filterspec $ journal l
− Commands/Register.hs
@@ -1,150 +0,0 @@-{-# LANGUAGE CPP #-}-{-| --A ledger-compatible @register@ command.---}--module Commands.Register (-  register- ,showRegisterReport- ,showPostingWithBalance- ,tests_Register-) where--import Safe (headMay, lastMay)-import Ledger-import Options-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding ( putStr )-import System.IO.UTF8-#endif----- | Print a register report.-register :: [Opt] -> [String] -> Ledger -> IO ()-register opts args l = do-  t <- getCurrentLocalTime-  putStr $ showRegisterReport opts (optsToFilterSpec opts args t) l---- | Generate the register report, which is a list of postings with transaction--- info and a running balance.-showRegisterReport :: [Opt] -> FilterSpec -> Ledger -> String-showRegisterReport opts filterspec l = showPostingsWithBalance ps nullposting startbal-    where-      ps | interval == NoInterval = displayableps-         | otherwise             = summarisePostings interval depth empty filterspan displayableps-      startbal = sumPostings precedingps-      (precedingps,displayableps,_) =-          postingsMatchingDisplayExpr (displayExprFromOpts opts) $ journalPostings $ filterJournalPostings filterspec $ journal l-      (interval, depth, empty) = (intervalFromOpts opts, depthFromOpts opts, Empty `elem` opts)-      filterspan = datespan filterspec---- | Convert a list of postings into summary postings, one per interval.-summarisePostings :: Interval -> Maybe Int -> Bool -> DateSpan -> [Posting] -> [Posting]-summarisePostings interval depth empty filterspan ps = concatMap summarisespan $ splitSpan interval reportspan-    where-      summarisespan s = summarisePostingsInDateSpan s depth empty (postingsinspan s)-      postingsinspan s = filter (isPostingInDateSpan s) ps-      dataspan = postingsDateSpan ps-      reportspan | empty = filterspan `orDatesFrom` dataspan-                 | otherwise = dataspan---- | Combine two datespans, filling any unspecified dates in the first--- with dates from the second.-orDatesFrom (DateSpan a1 b1) (DateSpan a2 b2) = DateSpan a b-    where a = if isJust a1 then a1 else a2-          b = if isJust b1 then b1 else b2---- | Date-sort and split a list of postings into three spans - postings matched--- by the given display expression, and the preceding and following postings.-postingsMatchingDisplayExpr :: Maybe String -> [Posting] -> ([Posting],[Posting],[Posting])-postingsMatchingDisplayExpr d ps = (before, matched, after)-    where -      sorted = sortBy (comparing postingDate) ps-      (before, rest) = break (displayExprMatches d) sorted-      (matched, after) = span (displayExprMatches d) rest---- | Does this display expression allow this posting to be displayed ?--- Raises an error if the display expression can't be parsed.-displayExprMatches :: Maybe String -> Posting -> Bool-displayExprMatches Nothing  _ = True-displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p-                        --- XXX confusing, refactor--- | Given a date span (representing a reporting interval) and a list of--- postings within it: aggregate the postings so there is only one per--- account, and adjust their date/description so that they will render--- as a summary for this interval.--- --- As usual with date spans the end date is exclusive, but for display--- purposes we show the previous day as end date, like ledger.--- --- When a depth argument is present, postings to accounts of greater--- depth are aggregated where possible.--- --- The showempty flag forces the display of a zero-posting span--- and also zero-posting accounts within the span.-summarisePostingsInDateSpan :: DateSpan -> Maybe Int -> Bool -> [Posting] -> [Posting]-summarisePostingsInDateSpan (DateSpan b e) depth showempty ps-    | null ps && (isNothing b || isNothing e) = []-    | null ps && showempty = [summaryp]-    | otherwise = summaryps'-    where-      summaryp = summaryPosting b' ("- "++ showDate (addDays (-1) e'))-      b' = fromMaybe (maybe nulldate postingDate $ headMay ps) b-      e' = fromMaybe (maybe (addDays 1 nulldate) postingDate $ lastMay ps) e-      summaryPosting date desc = nullposting{ptransaction=Just nulltransaction{tdate=date,tdescription=desc}}--      summaryps' = (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps-      summaryps = [summaryp{paccount=a,pamount=balancetoshowfor a} | a <- clippedanames]-      anames = sort $ nub $ map paccount ps-      -- aggregate balances by account, like cacheLedger, then do depth-clipping-      (_,_,exclbalof,inclbalof) = groupPostings ps-      clippedanames = nub $ map (clipAccountName d) anames-      isclipped a = accountNameLevel a >= d-      d = fromMaybe 99999 $ depth-      balancetoshowfor a =-          (if isclipped a then inclbalof else exclbalof) (if null a then "top" else a)--{- |-Show postings one per line, plus transaction info for the first posting of-each transaction, and a running balance. Eg:--@-date (10)  description (20)     account (22)            amount (11)  balance (12)-DDDDDDDDDD dddddddddddddddddddd aaaaaaaaaaaaaaaaaaaaaa  AAAAAAAAAAA AAAAAAAAAAAA-                                aaaaaaaaaaaaaaaaaaaaaa  AAAAAAAAAAA AAAAAAAAAAAA-@--}-showPostingsWithBalance :: [Posting] -> Posting -> MixedAmount -> String-showPostingsWithBalance [] _ _ = ""-showPostingsWithBalance (p:ps) pprev bal = this ++ showPostingsWithBalance ps p bal'-    where-      this = showPostingWithBalance isfirst p bal'-      isfirst = ptransaction p /= ptransaction pprev-      bal' = bal + pamount p---- | Show one posting and running balance, with or without transaction info.-showPostingWithBalance :: Bool -> Posting -> MixedAmount -> String-showPostingWithBalance withtxninfo p b = concatBottomPadded [txninfo ++ pstr ++ " ", bal] ++ "\n"-    where-      ledger3ishlayout = False-      datedescwidth = if ledger3ishlayout then 34 else 32-      txninfo = if withtxninfo then printf "%s %s " date desc else replicate datedescwidth ' '-      date = showDate da-      datewidth = 10-      descwidth = datedescwidth - datewidth - 2-      desc = printf ("%-"++(show descwidth)++"s") $ elideRight descwidth de :: String-      pstr = showPostingForRegister p-      bal = padleft 12 (showMixedAmountOrZeroWithoutPrice b)-      (da,de) = case ptransaction p of Just (Transaction{tdate=da',tdescription=de'}) -> (da',de')-                                       Nothing -> (nulldate,"")--tests_Register :: Test-tests_Register = TestList [--         "summarisePostings" ~: do-           summarisePostings Quarterly Nothing False (DateSpan Nothing Nothing) [] ~?= []--        ]
− Commands/Stats.hs
@@ -1,74 +0,0 @@-{-# LANGUAGE CPP #-}-{-|--Print some statistics for the ledger.---}--module Commands.Stats-where-import Ledger-import Options-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding ( putStr )-import System.IO.UTF8-#endif----- | Print various statistics for the ledger.-stats :: [Opt] -> [String] -> Ledger -> IO ()-stats opts args l = do-  today <- getCurrentDay-  putStr $ showStats opts args (cacheLedger' l) today--showStats :: [Opt] -> [String] -> Ledger -> Day -> String-showStats _ _ l today =-    heading ++ unlines (map (uncurry (printf fmt)) stats)-    where-      heading = underline $ printf "Ledger statistics as of %s" (show today)-      fmt = "%-" ++ show w1 ++ "s: %-" ++ show w2 ++ "s"-      w1 = maximum $ map (length . fst) stats-      w2 = maximum $ map (length . show . snd) stats-      stats = [-         ("File", filepath $ journal l)-        ,("Period", printf "%s to %s (%d days)" (start span) (end span) days)-        ,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed)-        ,("Transactions", printf "%d (%0.1f per day)" tnum txnrate)-        ,("Transactions last 30 days", printf "%d (%0.1f per day)" tnum30 txnrate30)-        ,("Transactions last 7 days", printf "%d (%0.1f per day)" tnum7 txnrate7)---        ,("Payees/descriptions", show $ length $ nub $ map tdescription ts)-        ,("Accounts", show $ length $ accounts l)-        ,("Account tree depth", show $ maximum $ map (accountNameLevel.aname) $ accounts l)-        ,("Commodities", printf "%s (%s)" (show $ length $ cs) (intercalate ", " $ sort $ map symbol cs)) -      -- Transactions this month     : %(monthtxns)s (last month in the same period: %(lastmonthtxns)s)-      -- Uncleared transactions      : %(uncleared)s-      -- Days since reconciliation   : %(reconcileelapsed)s-      -- Days since last transaction : %(recentelapsed)s-       ]-           where-             ts = sortBy (comparing tdate) $ jtxns $ journal l-             lastdate | null ts = Nothing-                      | otherwise = Just $ tdate $ last ts-             lastelapsed = maybe Nothing (Just . diffDays today) lastdate-             showelapsed Nothing = ""-             showelapsed (Just days) = printf " (%d %s)" days' direction-                                       where days' = abs days-                                             direction | days >= 0 = "days ago"-                                                       | otherwise = "days from now"-             tnum = length ts-             span = rawdatespan l-             start (DateSpan (Just d) _) = show d-             start _ = ""-             end (DateSpan _ (Just d)) = show d-             end _ = ""-             days = fromMaybe 0 $ daysInSpan span-             txnrate | days==0 = 0-                     | otherwise = fromIntegral tnum / fromIntegral days :: Double-             tnum30 = length $ filter withinlast30 ts-             withinlast30 t = d >= addDays (-30) today && (d<=today) where d = tdate t-             txnrate30 = fromIntegral tnum30 / 30 :: Double-             tnum7 = length $ filter withinlast7 ts-             withinlast7 t = d >= addDays (-7) today && (d<=today) where d = tdate t-             txnrate7 = fromIntegral tnum7 / 7 :: Double-             cs = commodities l-
− Commands/UI.hs
@@ -1,371 +0,0 @@-{-| --A simple text UI for hledger, based on the vty library.---}--module Commands.UI-where-import Safe (headDef)-import Graphics.Vty-import Ledger-import Options-import Commands.Balance-import Commands.Register-import Commands.Print---helpmsg = "(b)alance, (r)egister, (p)rint, (right) to drill down, (left) to back up, (q)uit"--instance Show Vty where show = const "a Vty"---- | The application state when running the ui command.-data AppState = AppState {-     av :: Vty                   -- ^ the vty context-    ,aw :: Int                  -- ^ window width-    ,ah :: Int                  -- ^ window height-    ,amsg :: String              -- ^ status message-    ,aopts :: [Opt]              -- ^ command-line opts-    ,aargs :: [String]           -- ^ command-line args-    ,aledger :: Ledger           -- ^ parsed ledger-    ,abuf :: [String]            -- ^ lines of the current buffered view-    ,alocs :: [Loc]              -- ^ user's navigation trail within the UI-                                -- ^ never null, head is current location-    } deriving (Show)---- | A location within the user interface.-data Loc = Loc {-     scr :: Screen               -- ^ one of the available screens-    ,sy :: Int                   -- ^ viewport y scroll position-    ,cy :: Int                   -- ^ cursor y position-    } deriving (Show)---- | The screens available within the user interface.-data Screen = BalanceScreen     -- ^ like hledger balance, shows accounts-            | RegisterScreen    -- ^ like hledger register, shows transaction-postings-            | PrintScreen       -- ^ like hledger print, shows ledger transactions-            -- | LedgerScreen      -- ^ shows the raw ledger-              deriving (Eq,Show)---- | Run the interactive text ui.-ui :: [Opt] -> [String] -> Ledger -> IO ()-ui opts args l = do-  v <- mkVty-  DisplayRegion w h <- display_bounds $ terminal v-  let opts' = SubTotal:opts-  t <-  getCurrentLocalTime-  let a = enter t BalanceScreen-          AppState {-                  av=v-                 ,aw=fromIntegral w-                 ,ah=fromIntegral h-                 ,amsg=helpmsg-                 ,aopts=opts'-                 ,aargs=args-                 ,aledger=l-                 ,abuf=[]-                 ,alocs=[]-                 }-  go a ---- | Update the screen, wait for the next event, repeat.-go :: AppState -> IO ()-go a@AppState{av=av,aopts=opts} = do-  when (notElem DebugNoUI opts) $ update av (renderScreen a)-  k <- next_event av-  t <- getCurrentLocalTime-  case k of -    EvResize x y                -> go $ resize x y a-    EvKey (KASCII 'l') [MCtrl]  -> refresh av >> go a{amsg=helpmsg}-    EvKey (KASCII 'b') []       -> go $ resetTrailAndEnter t BalanceScreen a-    EvKey (KASCII 'r') []       -> go $ resetTrailAndEnter t RegisterScreen a-    EvKey (KASCII 'p') []       -> go $ resetTrailAndEnter t PrintScreen a-    EvKey KRight []             -> go $ drilldown t a-    EvKey KEnter []             -> go $ drilldown t a-    EvKey KLeft  []             -> go $ backout t a-    EvKey KUp    []             -> go $ moveUpAndPushEdge a-    EvKey KDown  []             -> go $ moveDownAndPushEdge a-    EvKey KHome  []             -> go $ moveToTop a-    EvKey KUp    [MCtrl]        -> go $ moveToTop a-    EvKey KUp    [MShift]       -> go $ moveToTop a-    EvKey KEnd   []             -> go $ moveToBottom a-    EvKey KDown  [MCtrl]        -> go $ moveToBottom a-    EvKey KDown  [MShift]       -> go $ moveToBottom a-    EvKey KPageUp []            -> go $ prevpage a-    EvKey KBS []                -> go $ prevpage a-    EvKey (KASCII ' ') [MShift] -> go $ prevpage a-    EvKey KPageDown []          -> go $ nextpage a-    EvKey (KASCII ' ') []       -> go $ nextpage a-    EvKey (KASCII 'q') []       -> shutdown av >> return ()---    EvKey KEsc   []           -> shutdown av >> return ()-    _                           -> go a---- app state modifiers---- | The number of lines currently available for the main data display area.-pageHeight :: AppState -> Int-pageHeight a = ah a - 1--setLocCursorY, setLocScrollY :: Int -> Loc -> Loc-setLocCursorY y l = l{cy=y}-setLocScrollY y l = l{sy=y}--cursorY, scrollY, posY :: AppState -> Int-cursorY = cy . loc-scrollY = sy . loc-posY a = scrollY a + cursorY a--setCursorY, setScrollY, setPosY :: Int -> AppState -> AppState-setCursorY _ AppState{alocs=[]} = error "shouldn't happen" -- silence warnings-setCursorY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)} where l' = setLocCursorY y l--setScrollY _ AppState{alocs=[]} = error "shouldn't happen" -- silence warnings-setScrollY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)} where l' = setLocScrollY y l--setPosY _ AppState{alocs=[]}    = error "shouldn't happen" -- silence warnings-setPosY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)}-    where -      l' = setLocScrollY sy $ setLocCursorY cy l-      ph = pageHeight a-      cy = y `mod` ph-      sy = y - cy--updateCursorY, updateScrollY, updatePosY :: (Int -> Int) -> AppState -> AppState-updateCursorY f a = setCursorY (f $ cursorY a) a-updateScrollY f a = setScrollY (f $ scrollY a) a-updatePosY f a = setPosY (f $ posY a) a--resize :: Int -> Int -> AppState -> AppState-resize x y a = setCursorY cy' a{aw=x,ah=y}-    where-      cy = cursorY a-      cy' = min cy (y-2)--moveToTop :: AppState -> AppState-moveToTop = setPosY 0--moveToBottom :: AppState -> AppState-moveToBottom a = setPosY (length $ abuf a) a--moveUpAndPushEdge :: AppState -> AppState-moveUpAndPushEdge a-    | cy > 0 = updateCursorY (subtract 1) a-    | sy > 0 = updateScrollY (subtract 1) a-    | otherwise = a-    where Loc{sy=sy,cy=cy} = head $ alocs a--moveDownAndPushEdge :: AppState -> AppState-moveDownAndPushEdge a-    | sy+cy >= bh = a-    | cy < ph-1 = updateCursorY (+1) a-    | otherwise = updateScrollY (+1) a-    where -      Loc{sy=sy,cy=cy} = head $ alocs a-      ph = pageHeight a-      bh = length $ abuf a---- | Scroll down by page height or until we can just see the last line,--- without moving the cursor, or if we are already scrolled as far as--- possible then move the cursor to the last line.-nextpage :: AppState -> AppState-nextpage (a@AppState{abuf=b})-    | sy < bh-jump = setScrollY sy' a-    | otherwise    = setCursorY (bh-sy) a-    where-      sy = scrollY a-      jump = pageHeight a - 1-      bh = length b-      sy' = min (sy+jump) (bh-jump)---- | Scroll up by page height or until we can just see the first line,--- without moving the cursor, or if we are scrolled as far as possible--- then move the cursor to the first line.-prevpage :: AppState -> AppState-prevpage a-    | sy > 0    = setScrollY sy' a-    | otherwise = setCursorY 0 a-    where-      sy = scrollY a-      jump = pageHeight a - 1-      sy' = max (sy-jump) 0---- | Push a new UI location on to the stack.-pushLoc :: Loc -> AppState -> AppState-pushLoc l a = a{alocs=(l:alocs a)}--popLoc :: AppState -> AppState-popLoc a@AppState{alocs=locs}-    | length locs > 1 = a{alocs=drop 1 locs}-    | otherwise = a--clearLocs :: AppState -> AppState-clearLocs a = a{alocs=[]}--exit :: AppState -> AppState -exit = popLoc--loc :: AppState -> Loc-loc = head . alocs--screen :: AppState -> Screen-screen a = scr where (Loc scr _ _) = loc a---- | Enter a new screen, saving the old ui location on the stack.-enter :: LocalTime -> Screen -> AppState -> AppState-enter t scr@BalanceScreen a  = updateData t $ pushLoc Loc{scr=scr,sy=0,cy=0} a-enter t scr@RegisterScreen a = updateData t $ pushLoc Loc{scr=scr,sy=0,cy=0} a-enter t scr@PrintScreen a    = updateData t $ pushLoc Loc{scr=scr,sy=0,cy=0} a--resetTrailAndEnter t scr = enter t scr . clearLocs---- | Regenerate the display data appropriate for the current screen.-updateData :: LocalTime -> AppState -> AppState-updateData t a@AppState{aopts=opts,aargs=args,aledger=l} =-    case screen a of-      BalanceScreen  -> a{abuf=lines $ showBalanceReport opts (optsToFilterSpec opts args t) l, aargs=[]}-      RegisterScreen -> a{abuf=lines $ showRegisterReport opts (optsToFilterSpec opts args t) l}-      PrintScreen    -> a{abuf=lines $ showTransactions (optsToFilterSpec opts args t) l}--backout :: LocalTime -> AppState -> AppState-backout t a | screen a == BalanceScreen = a-            | otherwise = updateData t $ popLoc a--drilldown :: LocalTime -> AppState -> AppState-drilldown t a =-    case screen a of-      BalanceScreen  -> enter t RegisterScreen a{aargs=[currentAccountName a]}-      RegisterScreen -> scrollToTransaction e $ enter t PrintScreen a-      PrintScreen   -> a-    where e = currentTransaction a---- | Get the account name currently highlighted by the cursor on the--- balance screen. Results undefined while on other screens.-currentAccountName :: AppState -> AccountName-currentAccountName a = accountNameAt (abuf a) (posY a)---- | Get the full name of the account being displayed at a specific line--- within the balance command's output.-accountNameAt :: [String] -> Int -> AccountName-accountNameAt buf lineno = accountNameFromComponents anamecomponents-    where-      namestohere = map (drop 22) $ take (lineno+1) buf-      (indented, nonindented) = span (" " `isPrefixOf`) $ reverse namestohere-      thisbranch = indented ++ take 1 nonindented-      anamecomponents = reverse $ map strip $ dropsiblings thisbranch-      dropsiblings :: [AccountName] -> [AccountName]-      dropsiblings [] = []-      dropsiblings (x:xs) = x : dropsiblings xs'-          where-            xs' = dropWhile moreindented xs-            moreindented = (>= myindent) . indentof-            myindent = indentof x-            indentof = length . takeWhile (==' ')---- | If on the print screen, move the cursor to highlight the specified entry--- (or a reasonable guess). Doesn't work.-scrollToTransaction :: Maybe Transaction -> AppState -> AppState-scrollToTransaction Nothing a = a-scrollToTransaction (Just t) a@AppState{abuf=buf} = setCursorY cy $ setScrollY sy a-    where-      entryfirstline = head $ lines $ showTransaction t-      halfph = pageHeight a `div` 2-      y = fromMaybe 0 $ findIndex (== entryfirstline) buf-      sy = max 0 $ y - halfph-      cy = y - sy---- | Get the transaction containing the posting currently highlighted by--- the cursor on the register screen (or best guess). Results undefined--- while on other screens.-currentTransaction :: AppState -> Maybe Transaction-currentTransaction a@AppState{aledger=l,abuf=buf} = ptransaction p-    where-      p = headDef nullposting $ filter ismatch $ ledgerPostings l-      ismatch p = postingDate p == parsedate (take 10 datedesc)-                  && take 70 (showPostingWithBalance False p nullmixedamt) == (datedesc ++ acctamt)-      datedesc = take 32 $ fromMaybe "" $ find (not . (" " `isPrefixOf`)) $ headDef "" rest : reverse above-      acctamt = drop 32 $ headDef "" rest-      (above,rest) = splitAt y buf-      y = posY a---- renderers--renderScreen :: AppState -> Picture-renderScreen (a@AppState{aw=w,ah=h,abuf=buf,amsg=msg}) =-    Picture {pic_cursor = Cursor (fromIntegral cx) (fromIntegral cy)-            ,pic_image = mainimg-                         <->-                         renderStatus w msg-            ,pic_background = Background ' ' def_attr-            }-    where -      (cx, cy) = (0, cursorY a)-      sy = scrollY a---       mainimg = (renderString attr $ unlines $ above)---           <->---           (renderString reverseattr $ thisline)---           <->---           (renderString attr $ unlines $ below)---       (above,(thisline:below))---           | null ls   = ([],[""])---           | otherwise = splitAt y ls---       ls = lines $ fitto w (h-1) $ unlines $ drop as $ buf--- trying for more speed-      mainimg = vert_cat (map (string defaultattr) above)-               <->-               string currentlineattr thisline-               <->-               vert_cat (map (string defaultattr) below)-      (thisline,below) | null rest = (blankline,[])-                       | otherwise = (head rest, tail rest)-      (above,rest) = splitAt cy linestorender-      linestorender = map padclipline $ take (h-1) $ drop sy $ buf ++ replicate h blankline-      padclipline = take w . (++ blankline)-      blankline = replicate w ' '--padClipString :: Int -> Int -> String -> [String]-padClipString h w s = rows-    where-      rows = map padclipline $ take h $ lines s ++ replicate h blankline-      padclipline = take w . (++ blankline)-      blankline = replicate w ' '--renderString :: Attr -> String -> Image-renderString attr s = vert_cat $ map (string attr) rows-    where-      rows = lines $ fitto w h s-      w = maximum $ map length ls-      h = length ls-      ls = lines s--renderStatus :: Int -> String -> Image-renderStatus w = string statusattr . take w . (++ repeat ' ')---- the all-important theming engine!--theme = Restrained--data UITheme = Restrained | Colorful | Blood--(defaultattr, - currentlineattr, - statusattr- ) = case theme of-       Restrained -> (def_attr-                    ,def_attr `with_style` bold-                    ,def_attr `with_style` reverse_video-                    )-       Colorful   -> (def_attr `with_style` reverse_video-                    ,def_attr `with_fore_color` white `with_back_color` red-                    ,def_attr `with_fore_color` black `with_back_color` green-                    )-       Blood      -> (def_attr `with_style` reverse_video-                    ,def_attr `with_fore_color` white `with_back_color` red-                    ,def_attr `with_style` reverse_video-                    )--halfbrightattr = def_attr `with_style` dim-reverseattr = def_attr `with_style` reverse_video-redattr = def_attr `with_fore_color` red-greenattr = def_attr `with_fore_color` green-reverseredattr = def_attr `with_style` reverse_video `with_fore_color` red-reversegreenattr= def_attr `with_style` reverse_video `with_fore_color` green
− Commands/Web.hs
@@ -1,389 +0,0 @@-{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, MultiParamTypeClasses #-}-{-# OPTIONS_GHC -F -pgmFtrhsx #-}-{-| -A web-based UI.--}--module Commands.Web-where-#if __GLASGOW_HASKELL__ <= 610-import Codec.Binary.UTF8.String (decodeString)-#endif-import Control.Applicative.Error (Failing(Success,Failure))-import Control.Concurrent-import Control.Monad.Reader (ask)-import Data.IORef (newIORef, atomicModifyIORef)-import Network.HTTP (urlEncode, urlDecode)-import System.Directory (getModificationTime)-import System.IO.Storage (withStore, putValue, getValue)-import System.Time (ClockTime, getClockTime, diffClockTimes, TimeDiff(TimeDiff))-import Text.ParserCombinators.Parsec (parse)--import Hack.Contrib.Constants (_TextHtmlUTF8)-import Hack.Contrib.Response (set_content_type)-import qualified Hack (Env, http)-import qualified Hack.Contrib.Request (inputs, params, path)-import qualified Hack.Contrib.Response (redirect)-#ifdef WEBHAPPSTACK-import System.Process (readProcess)-import Hack.Handler.Happstack (runWithConfig,ServerConf(ServerConf))-#else-import Hack.Handler.SimpleServer (run)-#endif--import Network.Loli (loli, io, get, post, html, text, public)-import Network.Loli.Type (AppUnit)-import Network.Loli.Utils (update)--import HSP hiding (Request,catch)-import qualified HSP (Request(..))-import HSP.HTML (renderAsHTML)--import Commands.Add (ledgerAddTransaction)-import Commands.Balance-import Commands.Histogram-import Commands.Print-import Commands.Register-import Ledger-import Options hiding (value)-#ifdef MAKE-import Paths_hledger_make (getDataFileName)-#else-import Paths_hledger (getDataFileName)-#endif-import Utils (openBrowserOn)---- import Debug.Trace--- strace :: Show a => a -> a--- strace a = trace (show a) a--tcpport = 5000 :: Int-homeurl = printf "http://localhost:%d/" tcpport-browserdelay = 100000 -- microseconds--web :: [Opt] -> [String] -> Ledger -> IO ()-web opts args l = do-  unless (Debug `elem` opts) $ forkIO browser >> return ()-  server opts args l--browser :: IO ()-browser = putStrLn "starting web browser" >> threadDelay browserdelay >> openBrowserOn homeurl >> return ()--server :: [Opt] -> [String] -> Ledger -> IO ()-server opts args l =-  -- server initialisation-  withStore "hledger" $ do -- IO ()-    printf "starting web server on port %d\n" tcpport-    t <- getCurrentLocalTime-    webfiles <- getDataFileName "web"-    putValue "hledger" "ledger" l-#ifdef WEBHAPPSTACK-    hostname <- readProcess "hostname" [] "" `catch` \_ -> return "hostname"-    runWithConfig (ServerConf tcpport hostname) $            -- (Env -> IO Response) -> IO ()-#else-    run tcpport $            -- (Env -> IO Response) -> IO ()-#endif-      \env -> do -- IO Response-       -- general request handler-       let a = intercalate "+" $ reqparam env "a"-           p = intercalate "+" $ reqparam env "p"-           opts' = opts ++ [Period p]-           args' = args ++ (map urlDecode $ words a)-       l' <- fromJust `fmap` getValue "hledger" "ledger"-       l'' <- reloadIfChanged opts' args' l'-       -- declare path-specific request handlers-       let command :: [String] -> ([Opt] -> FilterSpec -> Ledger -> String) -> AppUnit-           command msgs f = string msgs $ f opts' (optsToFilterSpec opts' args' t) l''-       (loli $                                               -- State Loli () -> (Env -> IO Response)-         do-          get  "/balance"   $ command [] showBalanceReport  -- String -> ReaderT Env (StateT Response IO) () -> State Loli ()-          get  "/register"  $ command [] showRegisterReport-          get  "/histogram" $ command [] showHistogram-          get  "/transactions"   $ ledgerpage [] l'' (showTransactions (optsToFilterSpec opts' args' t))-          post "/transactions"   $ handleAddform l''-          get  "/env"       $ getenv >>= (text . show)-          get  "/params"    $ getenv >>= (text . show . Hack.Contrib.Request.params)-          get  "/inputs"    $ getenv >>= (text . show . Hack.Contrib.Request.inputs)-          public (Just webfiles) ["/style.css"]-          get  "/"          $ redirect ("transactions") Nothing-          ) env--getenv = ask-response = update-redirect u c = response $ Hack.Contrib.Response.redirect u c--reqparam :: Hack.Env -> String -> [String]-#if __GLASGOW_HASKELL__ <= 610-reqparam env p = map snd $ filter ((==p).fst) $ Hack.Contrib.Request.params env-#else-reqparam env p = map (decodeString.snd) $ filter ((==p).fst) $ Hack.Contrib.Request.params env-#endif--ledgerFileModifiedTime :: Ledger -> IO ClockTime-ledgerFileModifiedTime l-    | null path = getClockTime-    | otherwise = getModificationTime path `Prelude.catch` \_ -> getClockTime-    where path = filepath $ journal l--ledgerFileReadTime :: Ledger -> ClockTime-ledgerFileReadTime l = filereadtime $ journal l--reload :: Ledger -> IO Ledger-reload l = do-  l' <- readLedger (filepath $ journal l)-  putValue "hledger" "ledger" l'-  return l'-            -reloadIfChanged :: [Opt] -> [String] -> Ledger -> IO Ledger-reloadIfChanged opts _ l = do-  tmod <- ledgerFileModifiedTime l-  let tread = ledgerFileReadTime l-      newer = diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)-  -- when (Debug `elem` opts) $ printf "checking file, last modified %s, last read %s, %s\n" (show tmod) (show tread) (show newer)-  if newer-   then do-     when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" (filepath $ journal l)-     reload l-   else return l---- refilter :: [Opt] -> [String] -> Ledger -> LocalTime -> IO Ledger--- refilter opts args l t = return $ filterAndCacheLedgerWithOpts opts args t (jtext $ journal l) (journal l)--ledgerpage :: [String] -> Ledger -> (Ledger -> String) -> AppUnit-ledgerpage msgs l f = do-  env <- getenv-  l' <- io $ reloadIfChanged [] [] l-  hsp msgs $ const <div><% addform env %><pre><% f l' %></pre></div>---- | A loli directive to serve a string in pre tags within the hledger web--- layout.-string :: [String] -> String -> AppUnit-string msgs s = hsp msgs $ const <pre><% s %></pre>---- | A loli directive to serve a hsp template wrapped in the hledger web--- layout. The hack environment is passed in to every hsp template as an--- argument, since I don't see how to get it within the hsp monad.--- A list of messages is also passed, eg for form errors.-hsp :: [String] -> (Hack.Env -> HSP XML) -> AppUnit-hsp msgs f = do-  env <- getenv-  let contenthsp = f env-      pagehsp = hledgerpage env msgs title contenthsp-  html =<< (io $ do-              hspenv <- hackEnvToHspEnv env-              (_,xml) <- runHSP html4Strict pagehsp hspenv-              return $ addDoctype $ renderAsHTML xml)-  response $ set_content_type _TextHtmlUTF8-    where-      title = ""-      addDoctype = ("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n" ++)-      hackEnvToHspEnv :: Hack.Env -> IO HSPEnv-      hackEnvToHspEnv env = do-          x <- newIORef 0-          let req = HSP.Request (reqparam env) (Hack.http env)-              num = NumberGen (atomicModifyIORef x (\a -> (a+1,a)))-          return $ HSPEnv req num---- htmlToHsp :: Html -> HSP XML--- htmlToHsp h = return $ cdata $ showHtml h---- views--hledgerpage :: Hack.Env -> [String] -> String -> HSP XML -> HSP XML-hledgerpage env msgs title content =-    <html>-      <head>-        <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />-        <link rel="stylesheet" type="text/css" href="/style.css" media="all" />-        <title><% title %></title>-      </head>-      <body>-        <% navbar env %>-        <div id="messages"><% intercalate ", " msgs %></div>-        <div id="content"><% content %></div>-      </body>-    </html>--navbar :: Hack.Env -> HSP XML-navbar env =-    <div id="navbar">-      <a href="http://hledger.org" id="hledgerorglink">hledger.org</a>-      <% navlinks env %>-      <% searchform env %>-      <a href="http://hledger.org/MANUAL.html" id="helplink">help</a>-    </div>--#if __GLASGOW_HASKELL__ <= 610-getParamOrNull p = (decodeString . fromMaybe "") `fmap` getParam p-#else-getParamOrNull p = fromMaybe "" `fmap` getParam p-#endif--navlinks :: Hack.Env -> HSP XML-navlinks _ = do-   a <- getParamOrNull "a"-   p <- getParamOrNull "p"-   let addparams=(++(printf "?a=%s&p=%s" (urlEncode a) (urlEncode p)))-       link s = <a href=(addparams s) class="navlink"><% s %></a>-   <div id="navlinks">-     <% link "transactions" %> |-     <% link "register" %> |-     <% link "balance" %>-    </div>--searchform :: Hack.Env -> HSP XML-searchform env = do-   a <- getParamOrNull "a"-   p <- getParamOrNull "p"-   let resetlink | null a && null p = <span></span>-                 | otherwise = <span id="resetlink"><% nbsp %><a href=u>reset</a></span>-                 where u = dropWhile (=='/') $ Hack.Contrib.Request.path env-   <form action="" id="searchform">-      <% nbsp %>search for:<% nbsp %><input name="a" size="20" value=a-      /><% help "filter-patterns"-      %><% nbsp %><% nbsp %>in reporting period:<% nbsp %><input name="p" size="20" value=p-      /><% help "period-expressions"-      %><input type="submit" name="submit" value="filter" style="display:none" />-      <% resetlink %>-    </form>--addform :: Hack.Env -> HSP XML-addform env = do-  today <- io $ liftM showDate $ getCurrentDay-  let inputs = Hack.Contrib.Request.inputs env-#if __GLASGOW_HASKELL__ <= 610-      date  = decodeString $ fromMaybe today $ lookup "date"  inputs-      desc  = decodeString $ fromMaybe "" $ lookup "desc"  inputs-#else-      date  = fromMaybe today $ lookup "date"  inputs-      desc  = fromMaybe "" $ lookup "desc"  inputs-#endif-  <div>-   <div id="addform">-   <form action="" method="POST">-    <table border="0">-      <tr>-        <td>-          Date: <input size="15" name="date" value=date /><% help "dates" %><% nbsp %>-          Description: <input size="35" name="desc" value=desc /><% nbsp %>-        </td>-      </tr>-      <% transactionfields 1 env %>-      <% transactionfields 2 env %>-      <tr id="addbuttonrow"><td><input type="submit" value="add transaction" -      /><% help "file-format" %></td></tr>-    </table>-   </form>-   </div>-   <br clear="all" />-   </div>--help :: String -> HSP XML-help topic = <a href=u>?</a>-    where u = printf "http://hledger.org/MANUAL.html%s" l :: String-          l | null topic = ""-            | otherwise = '#':topic--transactionfields :: Int -> Hack.Env -> HSP XML-transactionfields n env = do-  let inputs = Hack.Contrib.Request.inputs env-#if __GLASGOW_HASKELL__ <= 610-      acct = decodeString $ fromMaybe "" $ lookup acctvar inputs-      amt  = decodeString $ fromMaybe "" $ lookup amtvar  inputs-#else-      acct = fromMaybe "" $ lookup acctvar inputs-      amt  = fromMaybe "" $ lookup amtvar  inputs-#endif-  <tr>-    <td>-    <% nbsp %><% nbsp %>-      Account: <input size="35" name=acctvar value=acct /><% nbsp %>-      Amount: <input size="15" name=amtvar value=amt /><% nbsp %>-    </td>-   </tr>-    where-      numbered = (++ show n)-      acctvar = numbered "acct"-      amtvar = numbered "amt"--handleAddform :: Ledger -> AppUnit-handleAddform l = do-  env <- getenv-  d <- io getCurrentDay-  t <- io getCurrentLocalTime-  handle t $ validate env d-  where-    validate :: Hack.Env -> Day -> Failing Transaction-    validate env today =-        let inputs = Hack.Contrib.Request.inputs env-#if __GLASGOW_HASKELL__ <= 610-            date  = decodeString $ fromMaybe "today" $ lookup "date"  inputs-            desc  = decodeString $ fromMaybe "" $ lookup "desc"  inputs-            acct1 = decodeString $ fromMaybe "" $ lookup "acct1" inputs-            amt1  = decodeString $ fromMaybe "" $ lookup "amt1"  inputs-            acct2 = decodeString $ fromMaybe "" $ lookup "acct2" inputs-            amt2  = decodeString $ fromMaybe "" $ lookup "amt2"  inputs-#else-            date  = fromMaybe "today" $ lookup "date"  inputs-            desc  = fromMaybe "" $ lookup "desc"  inputs-            acct1 = fromMaybe "" $ lookup "acct1" inputs-            amt1  = fromMaybe "" $ lookup "amt1"  inputs-            acct2 = fromMaybe "" $ lookup "acct2" inputs-            amt2  = fromMaybe "" $ lookup "amt2"  inputs-#endif-            validateDate ""  = ["missing date"]-            validateDate _   = []-            validateDesc ""  = ["missing description"]-            validateDesc _   = []-            validateAcct1 "" = ["missing account 1"]-            validateAcct1 _  = []-            validateAmt1 ""  = ["missing amount 1"]-            validateAmt1 _   = []-            validateAcct2 "" = ["missing account 2"]-            validateAcct2 _  = []-            validateAmt2 _   = []-            amt1' = either (const missingamt) id $ parse someamount "" amt1-            amt2' = either (const missingamt) id $ parse someamount "" amt2-            (date', dateparseerr) = case fixSmartDateStrEither today date of-                                      Right d -> (d, [])-                                      Left e -> ("1900/01/01", [showDateParseError e])-            t = Transaction {-                            tdate = parsedate date' -- date' must be parseable-                           ,teffectivedate=Nothing-                           ,tstatus=False-                           ,tcode=""-                           ,tdescription=desc-                           ,tcomment=""-                           ,tpostings=[-                             Posting False acct1 amt1' "" RegularPosting (Just t')-                            ,Posting False acct2 amt2' "" RegularPosting (Just t')-                            ]-                           ,tpreceding_comment_lines=""-                           }-            (t', balanceerr) = case balanceTransaction t of-                           Right t'' -> (t'', [])-                           Left e -> (t, [head $ lines e]) -- show just the error not the transaction-            errs = concat [-                    validateDate date-                   ,dateparseerr-                   ,validateDesc desc-                   ,validateAcct1 acct1-                   ,validateAmt1 amt1-                   ,validateAcct2 acct2-                   ,validateAmt2 amt2-                   ,balanceerr-                   ]-        in-        case null errs of-          False -> Failure errs-          True  -> Success t'--    handle :: LocalTime -> Failing Transaction -> AppUnit-    handle _ (Failure errs) = hsp errs addform-    handle ti (Success t)   = do-                    io $ ledgerAddTransaction l t >> reload l-                    ledgerpage [msg] l (showTransactions (optsToFilterSpec [] [] ti))-       where msg = printf "Added transaction:\n%s" (show t)--nbsp :: XML-nbsp = cdata "&nbsp;"
+ Hledger/Cli/Commands/Add.hs view
@@ -0,0 +1,198 @@+{-# LANGUAGE CPP #-}+{-| ++A history-aware add command to help with data entry.++-}++module Hledger.Cli.Commands.Add+where+import Hledger.Data+import Hledger.Cli.Options+import Hledger.Cli.Commands.Register (showRegisterReport)+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding (putStr, putStrLn, getLine, appendFile)+import System.IO.UTF8+import System.IO ( stderr, hFlush )+#else+import System.IO ( stderr, hFlush, hPutStrLn, hPutStr )+#endif+import System.IO.Error+import Text.ParserCombinators.Parsec+import Hledger.Cli.Utils (journalFromStringWithOpts)+import qualified Data.Foldable as Foldable (find)++-- | Read ledger transactions from the terminal, prompting for each field,+-- and append them to the ledger file. If the ledger came from stdin, this+-- command has no effect.+add :: [Opt] -> [String] -> Journal -> IO ()+add opts args j+    | filepath j == "-" = return ()+    | otherwise = do+  hPutStrLn stderr $+    "Enter one or more transactions, which will be added to your ledger file.\n"+    ++"To complete a transaction, enter . as account name. To quit, press control-c."+  today <- getCurrentDay+  getAndAddTransactions j opts args today `catch` (\e -> unless (isEOFError e) $ ioError e)++-- | Read a number of transactions from the command line, prompting,+-- validating, displaying and appending them to the journal file, until+-- end of input (then raise an EOF exception). Any command-line arguments+-- are used as the first transaction's description.+getAndAddTransactions :: Journal -> [Opt] -> [String] -> Day -> IO ()+getAndAddTransactions j opts args defaultDate = do+  (t, d) <- getTransaction j opts args defaultDate+  j <- journalAddTransaction j t+  getAndAddTransactions j opts args d++-- | Read a transaction from the command line, with history-aware prompting.+getTransaction :: Journal -> [Opt] -> [String] -> Day -> IO (Transaction,Day)+getTransaction j opts args defaultDate = do+  today <- getCurrentDay+  datestr <- askFor "date" +            (Just $ showDate defaultDate)+            (Just $ \s -> null s || +             isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s))+  description <- askFor "description" Nothing (Just $ not . null) +  let historymatches = transactionsSimilarTo j args description+      bestmatch | null historymatches = Nothing+                | otherwise = Just $ snd $ head historymatches+      bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch+      date = fixSmartDate today $ fromparse $ (parse smartdate "" . lowercase) datestr+      accept x = x == "." || (not . null) x &&+        if NoNewAccts `elem` opts+            then isJust $ Foldable.find (== x) ant+            else True+        where (ant,_,_,_) = groupPostings $ journalPostings j+      getpostingsandvalidate = do+        ps <- getPostings accept bestmatchpostings []+        let t = nulltransaction{tdate=date+                               ,tstatus=False+                               ,tdescription=description+                               ,tpostings=ps+                               }+            retry msg = do+              hPutStrLn stderr $ "\n" ++ msg ++ "please re-enter."+              getpostingsandvalidate+        either retry (return . flip (,) date) $ balanceTransaction t+  unless (null historymatches) +       (do+         hPutStrLn stderr "Similar transactions found, using the first for defaults:\n"+         hPutStr stderr $ concatMap (\(n,t) -> printf "[%3d%%] %s" (round $ n*100 :: Int) (show t)) $ take 3 historymatches)+  getpostingsandvalidate++-- | Read postings from the command line until . is entered, using the+-- provided historical postings, if any, to guess defaults.+getPostings :: (AccountName -> Bool) -> Maybe [Posting] -> [Posting] -> IO [Posting]+getPostings accept historicalps enteredps = do+  account <- askFor (printf "account %d" n) defaultaccount (Just accept)+  if account=="."+    then return enteredps+    else do+      amountstr <- askFor (printf "amount  %d" n) defaultamount validateamount+      let amount = fromparse $ parse (someamount <|> return missingamt) "" amountstr+      let p = nullposting{paccount=stripbrackets account,+                          pamount=amount,+                          ptype=postingtype account}+      getPostings accept historicalps $ enteredps ++ [p]+    where+      n = length enteredps + 1+      enteredrealps = filter isReal enteredps+      bestmatch | isNothing historicalps = Nothing+                | n <= length ps = Just $ ps !! (n-1)+                | otherwise = Nothing+                where Just ps = historicalps+      defaultaccount = maybe Nothing (Just . showacctname) bestmatch+      showacctname p = showAccountName Nothing (ptype p) $ paccount p+      defaultamount = maybe balancingamount (Just . show . pamount) bestmatch+          where balancingamount = Just $ show $ negate $ sumMixedAmountsPreservingHighestPrecision $ map pamount enteredrealps+      postingtype ('[':_) = BalancedVirtualPosting+      postingtype ('(':_) = VirtualPosting+      postingtype _ = RegularPosting+      stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse+      validateamount = Just $ \s -> (null s && not (null enteredrealps))+                                   || isRight (parse (someamount>>many spacenonewline>>eof) "" s)++-- | Prompt for and read a string value, optionally with a default value+-- and a validator. A validator causes the prompt to repeat until the+-- input is valid. May also raise an EOF exception if control-d is pressed.+askFor :: String -> Maybe String -> Maybe (String -> Bool) -> IO String+askFor prompt def validator = do+  hPutStr stderr $ prompt ++ maybe "" showdef def ++ ": "+  hFlush stderr+  l <- getLine+  let input = if null l then fromMaybe l def else l+  case validator of+    Just valid -> if valid input+                   then return input+                   else askFor prompt def validator+    Nothing -> return input+    where showdef s = " [" ++ s ++ "]"++-- | Append this transaction to the journal's file. Also, to the journal's+-- transaction list, but we don't bother updating the other fields - this+-- is enough to include new transactions in the history matching.+journalAddTransaction :: Journal -> Transaction -> IO Journal+journalAddTransaction j@Journal{jtxns=ts} t = do+  appendToJournalFile j $ showTransaction t+  putStrLn $ printf "\nAdded transaction to %s:" (filepath j)+  putStrLn =<< registerFromString (show t)+  return j{jtxns=ts++[t]}++-- | Append data to the journal's file, ensuring proper separation from+-- any existing data; or if the file is "-", dump it to stdout.+appendToJournalFile :: Journal -> String -> IO ()+appendToJournalFile Journal{filepath=f, jtext=t} s =+    if f == "-"+    then putStr $ sep ++ s+    else appendFile f $ sep++s+    where +      -- XXX we are looking at the original raw text from when the ledger+      -- was first read, but that's good enough for now+      sep | null $ strip t = ""+          | otherwise = replicate (2 - min 2 (length lastnls)) '\n'+          where lastnls = takeWhile (=='\n') $ reverse t++-- | Convert a string of ledger data into a register report.+registerFromString :: String -> IO String+registerFromString s = do+  now <- getCurrentLocalTime+  l <- journalFromStringWithOpts [] s+  return $ showRegisterReport opts (optsToFilterSpec opts [] now) l+    where opts = [Empty]++-- | Return a similarity measure, from 0 to 1, for two strings.+-- This is Simon White's letter pairs algorithm from+-- http://www.catalysoft.com/articles/StrikeAMatch.html+-- with a modification for short strings.+compareStrings :: String -> String -> Double+compareStrings "" "" = 1+compareStrings (_:[]) "" = 0+compareStrings "" (_:[]) = 0+compareStrings (a:[]) (b:[]) = if toUpper a == toUpper b then 1 else 0+compareStrings s1 s2 = 2.0 * fromIntegral i / fromIntegral u+    where+      i = length $ intersect pairs1 pairs2+      u = length pairs1 + length pairs2+      pairs1 = wordLetterPairs $ uppercase s1+      pairs2 = wordLetterPairs $ uppercase s2+wordLetterPairs = concatMap letterPairs . words+letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest)+letterPairs _ = []++compareDescriptions :: [Char] -> [Char] -> Double+compareDescriptions s t = compareStrings s' t'+    where s' = simplify s+          t' = simplify t+          simplify = filter (not . (`elem` "0123456789"))++transactionsSimilarTo :: Journal -> [String] -> String -> [(Double,Transaction)]+transactionsSimilarTo j apats s =+    sortBy compareRelevanceAndRecency+               $ filter ((> threshold).fst)+               [(compareDescriptions s $ tdescription t, t) | t <- ts]+    where+      compareRelevanceAndRecency (n1,t1) (n2,t2) = compare (n2,tdate t2) (n1,tdate t1)+      ts = jtxns $ filterJournalTransactionsByAccount apats j+      threshold = 0+
+ Hledger/Cli/Commands/All.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE CPP #-}+{-| ++The Commands package defines all the commands offered by the hledger+application, like \"register\" and \"balance\".  This module exports all+the commands; you can also import individual modules if you prefer.++-}++module Hledger.Cli.Commands.All (+                     module Hledger.Cli.Commands.Add,+                     module Hledger.Cli.Commands.Balance,+                     module Hledger.Cli.Commands.Convert,+                     module Hledger.Cli.Commands.Histogram,+                     module Hledger.Cli.Commands.Print,+                     module Hledger.Cli.Commands.Register,+                     module Hledger.Cli.Commands.Stats,+#ifdef VTY+                     module Hledger.Cli.Commands.Vty,+#endif+#if defined(WEB) || defined(WEBHAPPSTACK)+                     module Hledger.Cli.Commands.Web,+#endif+#ifdef CHART+                     module Hledger.Cli.Commands.Chart,+#endif+                     tests_Hledger_Commands+              )+where+import Hledger.Cli.Commands.Add+import Hledger.Cli.Commands.Balance+import Hledger.Cli.Commands.Convert+import Hledger.Cli.Commands.Histogram+import Hledger.Cli.Commands.Print+import Hledger.Cli.Commands.Register+import Hledger.Cli.Commands.Stats+#ifdef VTY+import Hledger.Cli.Commands.Vty+#endif+#if defined(WEB) || defined(WEBHAPPSTACK)+import Hledger.Cli.Commands.Web+#endif+#ifdef CHART+import Hledger.Cli.Commands.Chart+#endif+import Test.HUnit (Test(TestList))+++tests_Hledger_Commands = TestList+    [+--      Hledger.Cli.Commands.Add.tests_Add+--     ,Hledger.Cli.Commands.Balance.tests_Balance+     Hledger.Cli.Commands.Convert.tests_Convert+--     ,Hledger.Cli.Commands.Histogram.tests_Histogram+--     ,Hledger.Cli.Commands.Print.tests_Print+    ,Hledger.Cli.Commands.Register.tests_Register+--     ,Hledger.Cli.Commands.Stats.tests_Stats+    ]+-- #ifdef VTY+--     ,Hledger.Cli.Commands.Vty.tests_Vty+-- #endif+-- #if defined(WEB) || defined(WEBHAPPSTACK)+--     ,Hledger.Cli.Commands.Web.tests_Web+-- #endif+-- #ifdef CHART+--     ,Hledger.Cli.Commands.Chart.tests_Chart+-- #endif
+ Hledger/Cli/Commands/Balance.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE CPP #-}+{-|++A ledger-compatible @balance@ command.++ledger's balance command is easy to use but not easy to describe+precisely.  In the examples below we'll use sample.ledger, which has the+following account tree:++@+ assets+   bank+     checking+     saving+   cash+ expenses+   food+   supplies+ income+   gifts+   salary+ liabilities+   debts+@++The balance command shows accounts with their aggregate balances.+Subaccounts are displayed indented below their parent. Each balance is the+sum of any transactions in that account plus any balances from+subaccounts:++@+ $ hledger -f sample.ledger balance+                 $-1  assets+                  $1    bank:saving+                 $-2    cash+                  $2  expenses+                  $1    food+                  $1    supplies+                 $-2  income+                 $-1    gifts+                 $-1    salary+                  $1  liabilities:debts+@++Usually, the non-interesting accounts are elided or omitted. Above,+@checking@ is omitted because it has no subaccounts and a zero balance.+@bank@ is elided because it has only a single displayed subaccount+(@saving@) and it would be showing the same balance as that ($1). Ditto+for @liabilities@. We will return to this in a moment.++The --depth argument can be used to limit the depth of the balance report.+So, to see just the top level accounts:++@+$ hledger -f sample.ledger balance --depth 1+                 $-1  assets+                  $2  expenses+                 $-2  income+                  $1  liabilities+@++This time liabilities has no displayed subaccounts (due to --depth) and+is not elided.++With one or more account pattern arguments, the balance command shows+accounts whose name matches one of the patterns, plus their parents+(elided) and subaccounts. So with the pattern o we get:++@+ $ hledger -f sample.ledger balance o+                  $1  expenses:food+                 $-2  income+                 $-1    gifts+                 $-1    salary+--------------------+                 $-1+@++The o pattern matched @food@ and @income@, so they are shown. Unmatched+parents of matched accounts are also shown (elided) for context (@expenses@).++Also, the balance report shows the total of all displayed accounts, when+that is non-zero. Here, it is displayed because the accounts shown add up+to $-1.++Here is a more precise definition of \"interesting\" accounts in ledger's+balance report:++- an account which has just one interesting subaccount branch, and which+  is not at the report's maximum depth, is interesting if the balance is+  different from the subaccount's, and otherwise boring.++- any other account is interesting if it has a non-zero balance, or the -E+  flag is used.++-}++module Hledger.Cli.Commands.Balance+where+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Amount+import Hledger.Data.AccountName+import Hledger.Data.Posting+import Hledger.Data.Ledger+import Hledger.Cli.Options+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding ( putStr )+import System.IO.UTF8+#endif+++-- | Print a balance report.+balance :: [Opt] -> [String] -> Journal -> IO ()+balance opts args j = do+  t <- getCurrentLocalTime+  putStr $ showBalanceReport opts (optsToFilterSpec opts args t) j++-- | Generate a balance report with the specified options for this ledger.+showBalanceReport :: [Opt] -> FilterSpec -> Journal -> String+showBalanceReport opts filterspec j = acctsstr ++ totalstr+    where+      l = journalToLedger filterspec j+      acctsstr = unlines $ map showacct interestingaccts+          where+            showacct = showInterestingAccount l interestingaccts+            interestingaccts = filter (isInteresting opts l) acctnames+            acctnames = sort $ tail $ flatten $ treemap aname accttree+            accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l+      totalstr | NoTotal `elem` opts = ""+               | notElem Empty opts && isZeroMixedAmount total = ""+               | otherwise = printf "--------------------\n%s\n" $ padleft 20 $ showMixedAmountWithoutPrice total+          where+            total = sum $ map abalance $ ledgerTopAccounts l++-- | Display one line of the balance report with appropriate indenting and eliding.+showInterestingAccount :: Ledger -> [AccountName] -> AccountName -> String+showInterestingAccount l interestingaccts a = concatTopPadded [amt, "  ", depthspacer ++ partialname]+    where+      amt = padleft 20 $ showMixedAmountWithoutPrice $ abalance $ ledgerAccount l a+      parents = parentAccountNames a+      interestingparents = filter (`elem` interestingaccts) parents+      depthspacer = replicate (indentperlevel * length interestingparents) ' '+      indentperlevel = 2+      -- the partial name is the account's leaf name, prefixed by the+      -- names of any boring parents immediately above+      partialname = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a]+          where ps = takeWhile boring parents where boring = not . (`elem` interestingparents)++-- | Is the named account considered interesting for this ledger's balance report ?+isInteresting :: [Opt] -> Ledger -> AccountName -> Bool+isInteresting opts l a+    | numinterestingsubs==1 && not atmaxdepth = notlikesub+    | otherwise = notzero || emptyflag+    where+      atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depthFromOpts opts+      emptyflag = Empty `elem` opts+      acct = ledgerAccount l a+      notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct+      notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumPostings $ apostings acct+      numinterestingsubs = length $ filter isInterestingTree subtrees+          where+            isInterestingTree = treeany (isInteresting opts l . aname)+            subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a+
+ Hledger/Cli/Commands/Chart.hs view
@@ -0,0 +1,108 @@+{-|++Generate balances pie chart++-}++module Hledger.Cli.Commands.Chart+where+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Amount+import Hledger.Data.Ledger+import Hledger.Data.Commodity+import Hledger.Cli.Options++import Control.Monad (liftM3)+import Graphics.Rendering.Chart+import Data.Colour+import Data.Colour.Names+import Data.Colour.RGBSpace+import Data.Colour.RGBSpace.HSL (hsl)+import Data.Colour.SRGB.Linear (rgb)+import Data.List+import Safe (readDef)++-- | Generate an image with the pie chart and write it to a file+chart :: [Opt] -> [String] -> Journal -> IO ()+chart opts args j = do+  t <- getCurrentLocalTime+  let chart = genPie opts (optsToFilterSpec opts args t) j+  renderableToPNGFile (toRenderable chart) w h filename+    where+      filename = getOption opts ChartOutput chartoutput+      (w,h) = parseSize $ getOption opts ChartSize chartsize++-- | Extract string option value from a list of options or use the default+getOption :: [Opt] -> (String->Opt) -> String -> String+getOption opts opt def = +    case reverse $ optValuesForConstructor opt opts of+        [] -> def+        x:_ -> x++-- | Parse image size from a command-line option+parseSize :: String -> (Int,Int)+parseSize str = (read w, read h)+    where+    x = fromMaybe (error "Size should be in WIDTHxHEIGHT format") $ findIndex (=='x') str+    (w,_:h) = splitAt x str++-- | Generate pie chart+genPie :: [Opt] -> FilterSpec -> Journal -> PieLayout+genPie opts filterspec j = defaultPieLayout { pie_background_ = solidFillStyle $ opaque $ white+                                            , pie_plot_ = pie_chart }+    where+      pie_chart = defaultPieChart { pie_data_ = map (uncurry accountPieItem) chartitems'+                                  , pie_start_angle_ = (-90)+                                  , pie_colors_ = mkColours hue+                                  , pie_label_style_ = defaultFontStyle{font_size_=12}+                                  }+      chartitems' = debug "chart" $ top num samesignitems+      (samesignitems, sign) = sameSignNonZero rawitems+      rawitems = debug "raw" $ flatten $ balances $+                 ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) $ journalToLedger filterspec j+      top n t = topn ++ [other]+          where+            (topn,rest) = splitAt n $ reverse $ sortBy (comparing snd) t+            other = ("other", sum $ map snd rest)+      num = readDef (fromIntegral chartitems) (getOption opts ChartItems (show chartitems))+      hue = if sign > 0 then red else green where (red, green) = (0, 110)+      debug s = if Debug `elem` opts then ltrace s else id++-- | Select the nonzero items with same sign as the first, and make+-- them positive. Also return a 1 or -1 corresponding to the original sign.+sameSignNonZero :: [(AccountName, Double)] -> ([(AccountName, Double)], Int)+sameSignNonZero is | null nzs = ([], 1)+                   | otherwise = (map pos $ filter (test.snd) nzs, sign)+                   where+                     nzs = filter ((/=0).snd) is+                     pos (a,b) = (a, abs b)+                     sign = if snd (head nzs) >= 0 then 1 else (-1)+                     test = if sign > 0 then (>0) else (<0)++-- | Convert all quantities of MixedAccount to a single commodity+amountValue :: MixedAmount -> Double+amountValue = quantity . convertMixedAmountTo unknown++-- | Generate a tree of account names together with their balances.+--   The balance of account is decremented by the balance of its subaccounts+--   which are drawn on the chart.+balances :: Tree Account -> Tree (AccountName, Double)+balances (Node rootAcc subAccs) = Node newroot newsubs+    where+      newroot = (aname rootAcc,+                 amountValue $+                 abalance rootAcc - (sum . map (abalance . root)) subAccs)+      newsubs = map balances subAccs++-- | Build a single pie chart item+accountPieItem :: AccountName -> Double -> PieItem+accountPieItem accname balance = PieItem accname offset balance where offset = 0++-- | Generate an infinite color list suitable for charts.+mkColours :: Double -> [AlphaColour Double]+mkColours hue = cycle $ [opaque $ rgbToColour $ hsl h s l | (h,s,l) <- liftM3 (,,)+                         [hue] [0.7] [0.1,0.2..0.7] ]++rgbToColour :: (Fractional a) => RGB a -> Colour a+rgbToColour (RGB r g b) = rgb r g b
+ Hledger/Cli/Commands/Convert.hs view
@@ -0,0 +1,386 @@+{-|+Convert account data in CSV format (eg downloaded from a bank) to ledger+format, and print it on stdout. See the manual for more details.+-}++module Hledger.Cli.Commands.Convert where+import Hledger.Cli.Options (Opt(Debug))+import Hledger.Cli.Version (versionstr)+import Hledger.Data.Types (Journal,AccountName,Transaction(..),Posting(..),PostingType(..))+import Hledger.Data.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual)+import Hledger.Data.Parse (someamount, emptyCtx, ledgeraccountname)+import Hledger.Data.Amount (nullmixedamt)+import Safe (atDef, maximumDef)+import System.IO (stderr)+import Text.CSV (parseCSVFromFile, printCSV)+import Text.Printf (hPrintf)+import Text.RegexPR (matchRegexPR, gsubRegexPR)+import Data.Maybe+import Hledger.Data.Dates (firstJust, showDate, parsedate)+import System.Locale (defaultTimeLocale)+import Data.Time.Format (parseTime)+import Control.Monad (when, guard, liftM)+import Safe (readDef, readMay)+import System.Directory (doesFileExist)+import System.Exit (exitFailure)+import System.FilePath.Posix (takeBaseName, replaceExtension)+import Text.ParserCombinators.Parsec+import Test.HUnit+++{- |+A set of data definitions and account-matching patterns sufficient to+convert a particular CSV data file into meaningful ledger transactions. See above.+-}+data CsvRules = CsvRules {+      dateField :: Maybe FieldPosition,+      statusField :: Maybe FieldPosition,+      codeField :: Maybe FieldPosition,+      descriptionField :: Maybe FieldPosition,+      amountField :: Maybe FieldPosition,+      currencyField :: Maybe FieldPosition,+      baseCurrency :: Maybe String,+      baseAccount :: AccountName,+      accountRules :: [AccountRule]+} deriving (Show, Eq)++nullrules = CsvRules {+      dateField=Nothing,+      statusField=Nothing,+      codeField=Nothing,+      descriptionField=Nothing,+      amountField=Nothing,+      currencyField=Nothing,+      baseCurrency=Nothing,+      baseAccount="unknown",+      accountRules=[]+}++type FieldPosition = Int++type AccountRule = (+   [(String, Maybe String)] -- list of regex match patterns with optional replacements+  ,AccountName              -- account name to use for a transaction matching this rule+  )++type CsvRecord = [String]+++-- | Read the CSV file named as an argument and print equivalent journal transactions,+-- using/creating a .rules file.+convert :: [Opt] -> [String] -> Journal -> IO ()+convert opts args _ = do+  when (null args) $ error "please specify a csv data file."+  let csvfile = head args+  csvparse <- parseCSVFromFile csvfile+  let records = case csvparse of+                  Left e -> error $ show e+                  Right rs -> reverse $ filter (/= [""]) rs+  let debug = Debug `elem` opts+      rulesfile = rulesFileFor csvfile+  exists <- doesFileExist rulesfile+  if (not exists) then do+                  hPrintf stderr "creating conversion rules file %s, edit this file for better results\n" rulesfile+                  writeFile rulesfile initialRulesFileContent+   else+      hPrintf stderr "using conversion rules file %s\n" rulesfile+  rules <- liftM (either (error.show) id) $ parseCsvRulesFile rulesfile+  when debug $ hPrintf stderr "rules: %s\n" (show rules)+  let requiredfields = max 2 (maxFieldIndex rules + 1)+      badrecords = take 1 $ filter ((< requiredfields).length) records+  if null badrecords+   then mapM_ (printTxn debug rules) records+   else do+     hPrintf stderr (unlines [+                      "Warning, at least one CSV record does not contain a field referenced by the"+                     ,"conversion rules file, or has less than two fields. Are you converting a"+                     ,"valid CSV file ? First bad record:\n%s"+                     ]) (show $ head badrecords)+     exitFailure++-- | The highest (0-based) field index referenced in the field+-- definitions, or -1 if no fields are defined.+maxFieldIndex :: CsvRules -> Int+maxFieldIndex r = maximumDef (-1) $ catMaybes [+                   dateField r+                  ,statusField r+                  ,codeField r+                  ,descriptionField r+                  ,amountField r+                  ,currencyField r+                  ]++rulesFileFor :: FilePath -> FilePath+rulesFileFor csvfile = replaceExtension csvfile ".rules"++initialRulesFileContent :: String+initialRulesFileContent =+    "# csv conversion rules file generated by hledger "++versionstr++"\n" +++    "# Add rules to this file for more accurate conversion, see\n"+++    "# http://hledger.org/MANUAL.html#convert\n" +++    "\n" +++    "base-account assets:bank:checking\n" +++    "date-field 0\n" +++    "description-field 4\n" +++    "amount-field 1\n" +++    "currency $\n" +++    "\n" +++    "# account-assigning rules\n" +++    "\n" +++    "SPECTRUM\n" +++    "expenses:health:gym\n" +++    "\n" +++    "ITUNES\n" +++    "BLKBSTR=BLOCKBUSTER\n" +++    "expenses:entertainment\n" +++    "\n" +++    "(TO|FROM) SAVINGS\n" +++    "assets:bank:savings\n"++-- rules file parser++parseCsvRulesFile :: FilePath -> IO (Either ParseError CsvRules)+parseCsvRulesFile f = do+  s <- readFile f+  return $ parseCsvRules f s++parseCsvRules :: FilePath -> String -> Either ParseError CsvRules+parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s++csvrulesfile :: GenParser Char CsvRules CsvRules+csvrulesfile = do+  many blankorcommentline+  many definitions+  r <- getState+  ars <- many accountrule+  many blankorcommentline+  eof+  return r{accountRules=ars}++-- | Real independent parser choice, even when alternative matches share a prefix.+choice' parsers = choice $ map try (init parsers) ++ [last parsers]++definitions :: GenParser Char CsvRules ()+definitions = do+  choice' [+    datefield+   ,statusfield+   ,codefield+   ,descriptionfield+   ,amountfield+   ,currencyfield+   ,basecurrency+   ,baseaccount+   ,commentline+   ] <?> "definition"+  return ()++datefield = do+  string "date-field"+  many1 spacenonewline+  v <- restofline+  r <- getState+  setState r{dateField=readMay v}++codefield = do+  string "code-field"+  many1 spacenonewline+  v <- restofline+  r <- getState+  setState r{codeField=readMay v}++statusfield = do+  string "status-field"+  many1 spacenonewline+  v <- restofline+  r <- getState+  setState r{statusField=readMay v}++descriptionfield = do+  string "description-field"+  many1 spacenonewline+  v <- restofline+  r <- getState+  setState r{descriptionField=readMay v}++amountfield = do+  string "amount-field"+  many1 spacenonewline+  v <- restofline+  r <- getState+  setState r{amountField=readMay v}++currencyfield = do+  string "currency-field"+  many1 spacenonewline+  v <- restofline+  r <- getState+  setState r{currencyField=readMay v}++basecurrency = do+  string "currency"+  many1 spacenonewline+  v <- restofline+  r <- getState+  setState r{baseCurrency=Just v}++baseaccount = do+  string "base-account"+  many1 spacenonewline+  v <- ledgeraccountname+  optional newline+  r <- getState+  setState r{baseAccount=v}++accountrule :: GenParser Char CsvRules AccountRule+accountrule = do+  many blankorcommentline+  pats <- many1 matchreplacepattern+  guard $ length pats >= 2+  let pats' = init pats+      acct = either (fail.show) id $ runParser ledgeraccountname () "" $ fst $ last pats+  many blankorcommentline+  return (pats',acct)+ <?> "account rule"++blanklines = many1 blankline >> return ()++blankline = many spacenonewline >> newline >> return () <?> "blank line"++commentchar = oneOf ";#"++commentline = many spacenonewline >> commentchar >> restofline >> return () <?> "comment line"++blankorcommentline = choice' [blankline, commentline]++matchreplacepattern = do+  notFollowedBy commentchar+  matchpat <- many1 (noneOf "=\n")+  replpat <- optionMaybe $ do {char '='; many $ noneOf "\n"}+  newline+  return (matchpat,replpat)++printTxn :: Bool -> CsvRules -> CsvRecord -> IO ()+printTxn debug rules rec = do+  when debug $ hPrintf stderr "record: %s" (printCSV [rec])+  putStr $ show $ transactionFromCsvRecord rules rec++-- csv record conversion++transactionFromCsvRecord :: CsvRules -> CsvRecord -> Transaction+transactionFromCsvRecord rules fields =+  let +      date = parsedate $ normaliseDate $ maybe "1900/1/1" (atDef "" fields) (dateField rules)+      status = maybe False (null . strip . (atDef "" fields)) (statusField rules)+      code = maybe "" (atDef "" fields) (codeField rules)+      desc = maybe "" (atDef "" fields) (descriptionField rules)+      comment = ""+      precomment = ""+      amountstr = maybe "" (atDef "" fields) (amountField rules)+      amountstr' = strnegate amountstr where strnegate ('-':s) = s+                                             strnegate s = '-':s+      currency = maybe (fromMaybe "" $ baseCurrency rules) (atDef "" fields) (currencyField rules)+      amountstr'' = currency ++ amountstr'+      amountparse = runParser someamount emptyCtx "" amountstr''+      amount = either (const nullmixedamt) id amountparse+      unknownacct | (readDef 0 amountstr' :: Double) < 0 = "income:unknown"+                  | otherwise = "expenses:unknown"+      (acct,newdesc) = identify (accountRules rules) unknownacct desc+      t = Transaction {+              tdate=date,+              teffectivedate=Nothing,+              tstatus=status,+              tcode=code,+              tdescription=newdesc,+              tcomment=comment,+              tpreceding_comment_lines=precomment,+              tpostings=[+                   Posting {+                     pstatus=False,+                     paccount=acct,+                     pamount=amount,+                     pcomment="",+                     ptype=RegularPosting,+                     ptransaction=Just t+                   },+                   Posting {+                     pstatus=False,+                     paccount=baseAccount rules,+                     pamount=(-amount),+                     pcomment="",+                     ptype=RegularPosting,+                     ptransaction=Just t+                   }+                  ]+            }+  in t++-- | Convert some date string with unknown format to YYYY/MM/DD.+normaliseDate :: String -> String+normaliseDate s = maybe "0000/00/00" showDate $+              firstJust+              [parseTime defaultTimeLocale "%Y/%m/%e" s+               -- can't parse a month without leading 0, try adding one+              ,parseTime defaultTimeLocale "%Y/%m/%e" (take 5 s ++ "0" ++ drop 5 s)+              ,parseTime defaultTimeLocale "%Y-%m-%e" s+              ,parseTime defaultTimeLocale "%Y-%m-%e" (take 5 s ++ "0" ++ drop 5 s)+              ,parseTime defaultTimeLocale "%m/%e/%Y" s+              ,parseTime defaultTimeLocale "%m/%e/%Y" ('0':s)+              ,parseTime defaultTimeLocale "%m-%e-%Y" s+              ,parseTime defaultTimeLocale "%m-%e-%Y" ('0':s)+              ]++-- | Apply account matching rules to a transaction description to obtain+-- the most appropriate account and a new description.+identify :: [AccountRule] -> String -> String -> (String,String)+identify rules defacct desc | null matchingrules = (defacct,desc)+                            | otherwise = (acct,newdesc)+    where+      matchingrules = filter ismatch rules :: [AccountRule]+          where ismatch = any (isJust . flip matchRegexPR (caseinsensitive desc) . fst) . fst+      (prs,acct) = head matchingrules+      p_ms_r = filter (\(_,m,_) -> isJust m) $ map (\(p,r) -> (p, matchRegexPR (caseinsensitive p) desc, r)) prs+      (p,_,r) = head p_ms_r+      newdesc = case r of Just rpat -> gsubRegexPR (caseinsensitive p) rpat desc+                          Nothing   -> desc++caseinsensitive = ("(?i)"++)++tests_Convert = TestList [++   "convert rules parsing: empty file" ~: do+     -- let assertMixedAmountParse parseresult mixedamount =+     --         (either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)+    assertParseEqual (parseCsvRules "unknown" "") nullrules++  ,"convert rules parsing: accountrule" ~: do+     assertParseEqual (parseWithCtx nullrules accountrule "A\na\n") -- leading blank line required+                 ([("A",Nothing)], "a")++  ,"convert rules parsing: trailing comments" ~: do+     assertParse (parseWithCtx nullrules csvrulesfile "A\na\n# \n#\n")++  ,"convert rules parsing: trailing blank lines" ~: do+     assertParse (parseWithCtx nullrules csvrulesfile "A\na\n\n  \n")++  -- not supported+  -- ,"convert rules parsing: no final newline" ~: do+  --    assertParse (parseWithCtx nullrules csvrulesfile "A\na")+  --    assertParse (parseWithCtx nullrules csvrulesfile "A\na\n# \n#")+  --    assertParse (parseWithCtx nullrules csvrulesfile "A\na\n\n  ")++                 -- (nullrules{+                 --   -- dateField=Maybe FieldPosition,+                 --   -- statusField=Maybe FieldPosition,+                 --   -- codeField=Maybe FieldPosition,+                 --   -- descriptionField=Maybe FieldPosition,+                 --   -- amountField=Maybe FieldPosition,+                 --   -- currencyField=Maybe FieldPosition,+                 --   -- baseCurrency=Maybe String,+                 --   -- baseAccount=AccountName,+                 --   accountRules=[+                 --        ([("A",Nothing)], "a")+                 --       ]+                 --  })++  ]
+ Hledger/Cli/Commands/Histogram.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE CPP #-}+{-| ++Print a histogram report.++-}++module Hledger.Cli.Commands.Histogram+where+import Hledger.Data+import Hledger.Cli.Options+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding ( putStr )+import System.IO.UTF8+#endif+++barchar = '*'++-- | Print a histogram of some statistic per reporting interval, such as+-- number of postings per day.+histogram :: [Opt] -> [String] -> Journal -> IO ()+histogram opts args j = do+  t <- getCurrentLocalTime+  putStr $ showHistogram opts (optsToFilterSpec opts args t) j++showHistogram :: [Opt] -> FilterSpec -> Journal -> String+showHistogram opts filterspec j = concatMap (printDayWith countBar) dayps+    where+      i = intervalFromOpts opts+      interval | i == NoInterval = Daily+               | otherwise = i+      fullspan = journalDateSpan j+      days = filter (DateSpan Nothing Nothing /=) $ splitSpan interval fullspan+      dayps = [(s, filter (isPostingInDateSpan s) ps) | s <- days]+      -- same as Register+      -- should count transactions, not postings ?+      ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j+      filterempties+          | Empty `elem` opts = id+          | otherwise = filter (not . isZeroMixedAmount . pamount)+      matchapats = matchpats apats . paccount+      apats = acctpats filterspec+      filterdepth | interval == NoInterval = filter (\p -> accountNameLevel (paccount p) <= depth)+                  | otherwise = id+      depth = fromMaybe 99999 $ depthFromOpts opts++printDayWith f (DateSpan b _, ts) = printf "%s %s\n" (show $ fromJust b) (f ts)++countBar ps = replicate (length ps) barchar
+ Hledger/Cli/Commands/Print.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE CPP #-}+{-| ++A ledger-compatible @print@ command.++-}++module Hledger.Cli.Commands.Print+where+import Hledger.Data+import Hledger.Cli.Options+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding ( putStr )+import System.IO.UTF8+#endif+++-- | Print ledger transactions in standard format.+print' :: [Opt] -> [String] -> Journal -> IO ()+print' opts args j = do+  t <- getCurrentLocalTime+  putStr $ showTransactions (optsToFilterSpec opts args t) j++showTransactions :: FilterSpec -> Journal -> String+showTransactions filterspec j =+    concatMap (showTransactionForPrint effective) $ sortBy (comparing tdate) txns+        where+          effective = EffectiveDate == whichdate filterspec+          txns = jtxns $ filterJournalTransactions filterspec j
+ Hledger/Cli/Commands/Register.hs view
@@ -0,0 +1,150 @@+{-# LANGUAGE CPP #-}+{-| ++A ledger-compatible @register@ command.++-}++module Hledger.Cli.Commands.Register (+  register+ ,showRegisterReport+ ,showPostingWithBalance+ ,tests_Register+) where++import Safe (headMay, lastMay)+import Hledger.Data+import Hledger.Cli.Options+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding ( putStr )+import System.IO.UTF8+#endif+++-- | Print a register report.+register :: [Opt] -> [String] -> Journal -> IO ()+register opts args j = do+  t <- getCurrentLocalTime+  putStr $ showRegisterReport opts (optsToFilterSpec opts args t) j++-- | Generate the register report, which is a list of postings with transaction+-- info and a running balance.+showRegisterReport :: [Opt] -> FilterSpec -> Journal -> String+showRegisterReport opts filterspec j = showPostingsWithBalance ps nullposting startbal+    where+      ps | interval == NoInterval = displayableps+         | otherwise             = summarisePostings interval depth empty filterspan displayableps+      startbal = sumPostings precedingps+      (precedingps,displayableps,_) =+          postingsMatchingDisplayExpr (displayExprFromOpts opts) $ journalPostings $ filterJournalPostings filterspec j+      (interval, depth, empty) = (intervalFromOpts opts, depthFromOpts opts, Empty `elem` opts)+      filterspan = datespan filterspec++-- | Convert a list of postings into summary postings, one per interval.+summarisePostings :: Interval -> Maybe Int -> Bool -> DateSpan -> [Posting] -> [Posting]+summarisePostings interval depth empty filterspan ps = concatMap summarisespan $ splitSpan interval reportspan+    where+      summarisespan s = summarisePostingsInDateSpan s depth empty (postingsinspan s)+      postingsinspan s = filter (isPostingInDateSpan s) ps+      dataspan = postingsDateSpan ps+      reportspan | empty = filterspan `orDatesFrom` dataspan+                 | otherwise = dataspan++-- | Combine two datespans, filling any unspecified dates in the first+-- with dates from the second.+orDatesFrom (DateSpan a1 b1) (DateSpan a2 b2) = DateSpan a b+    where a = if isJust a1 then a1 else a2+          b = if isJust b1 then b1 else b2++-- | Date-sort and split a list of postings into three spans - postings matched+-- by the given display expression, and the preceding and following postings.+postingsMatchingDisplayExpr :: Maybe String -> [Posting] -> ([Posting],[Posting],[Posting])+postingsMatchingDisplayExpr d ps = (before, matched, after)+    where +      sorted = sortBy (comparing postingDate) ps+      (before, rest) = break (displayExprMatches d) sorted+      (matched, after) = span (displayExprMatches d) rest++-- | Does this display expression allow this posting to be displayed ?+-- Raises an error if the display expression can't be parsed.+displayExprMatches :: Maybe String -> Posting -> Bool+displayExprMatches Nothing  _ = True+displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p+                        +-- XXX confusing, refactor+-- | Given a date span (representing a reporting interval) and a list of+-- postings within it: aggregate the postings so there is only one per+-- account, and adjust their date/description so that they will render+-- as a summary for this interval.+-- +-- As usual with date spans the end date is exclusive, but for display+-- purposes we show the previous day as end date, like ledger.+-- +-- When a depth argument is present, postings to accounts of greater+-- depth are aggregated where possible.+-- +-- The showempty flag forces the display of a zero-posting span+-- and also zero-posting accounts within the span.+summarisePostingsInDateSpan :: DateSpan -> Maybe Int -> Bool -> [Posting] -> [Posting]+summarisePostingsInDateSpan (DateSpan b e) depth showempty ps+    | null ps && (isNothing b || isNothing e) = []+    | null ps && showempty = [summaryp]+    | otherwise = summaryps'+    where+      summaryp = summaryPosting b' ("- "++ showDate (addDays (-1) e'))+      b' = fromMaybe (maybe nulldate postingDate $ headMay ps) b+      e' = fromMaybe (maybe (addDays 1 nulldate) postingDate $ lastMay ps) e+      summaryPosting date desc = nullposting{ptransaction=Just nulltransaction{tdate=date,tdescription=desc}}++      summaryps' = (if showempty then id else filter (not . isZeroMixedAmount . pamount)) summaryps+      summaryps = [summaryp{paccount=a,pamount=balancetoshowfor a} | a <- clippedanames]+      anames = sort $ nub $ map paccount ps+      -- aggregate balances by account, like journalToLedger, then do depth-clipping+      (_,_,exclbalof,inclbalof) = groupPostings ps+      clippedanames = nub $ map (clipAccountName d) anames+      isclipped a = accountNameLevel a >= d+      d = fromMaybe 99999 $ depth+      balancetoshowfor a =+          (if isclipped a then inclbalof else exclbalof) (if null a then "top" else a)++{- |+Show postings one per line, plus transaction info for the first posting of+each transaction, and a running balance. Eg:++@+date (10)  description (20)     account (22)            amount (11)  balance (12)+DDDDDDDDDD dddddddddddddddddddd aaaaaaaaaaaaaaaaaaaaaa  AAAAAAAAAAA AAAAAAAAAAAA+                                aaaaaaaaaaaaaaaaaaaaaa  AAAAAAAAAAA AAAAAAAAAAAA+@+-}+showPostingsWithBalance :: [Posting] -> Posting -> MixedAmount -> String+showPostingsWithBalance [] _ _ = ""+showPostingsWithBalance (p:ps) pprev bal = this ++ showPostingsWithBalance ps p bal'+    where+      this = showPostingWithBalance isfirst p bal'+      isfirst = ptransaction p /= ptransaction pprev+      bal' = bal + pamount p++-- | Show one posting and running balance, with or without transaction info.+showPostingWithBalance :: Bool -> Posting -> MixedAmount -> String+showPostingWithBalance withtxninfo p b = concatBottomPadded [txninfo ++ pstr ++ " ", bal] ++ "\n"+    where+      ledger3ishlayout = False+      datedescwidth = if ledger3ishlayout then 34 else 32+      txninfo = if withtxninfo then printf "%s %s " date desc else replicate datedescwidth ' '+      date = showDate da+      datewidth = 10+      descwidth = datedescwidth - datewidth - 2+      desc = printf ("%-"++(show descwidth)++"s") $ elideRight descwidth de :: String+      pstr = showPostingForRegister p+      bal = padleft 12 (showMixedAmountOrZeroWithoutPrice b)+      (da,de) = case ptransaction p of Just (Transaction{tdate=da',tdescription=de'}) -> (da',de')+                                       Nothing -> (nulldate,"")++tests_Register :: Test+tests_Register = TestList [++         "summarisePostings" ~: do+           summarisePostings Quarterly Nothing False (DateSpan Nothing Nothing) [] ~?= []++        ]
+ Hledger/Cli/Commands/Stats.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE CPP #-}+{-|++Print some statistics for the ledger.++-}++module Hledger.Cli.Commands.Stats+where+import Hledger.Data+import Hledger.Cli.Options+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding ( putStr )+import System.IO.UTF8+#endif+import qualified Data.Map as Map+++-- | Print various statistics for the ledger.+stats :: [Opt] -> [String] -> Journal -> IO ()+stats opts args j = do+  today <- getCurrentDay+  putStr $ showStats opts args (journalToLedger nullfilterspec j) today++showStats :: [Opt] -> [String] -> Ledger -> Day -> String+showStats _ _ l today =+    heading ++ unlines (map (uncurry (printf fmt)) stats)+    where+      heading = underline $ printf "Ledger statistics as of %s" (show today)+      fmt = "%-" ++ show w1 ++ "s: %-" ++ show w2 ++ "s"+      w1 = maximum $ map (length . fst) stats+      w2 = maximum $ map (length . show . snd) stats+      stats = [+         ("File", filepath $ journal l)+        ,("Period", printf "%s to %s (%d days)" (start span) (end span) days)+        ,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed)+        ,("Transactions", printf "%d (%0.1f per day)" tnum txnrate)+        ,("Transactions last 30 days", printf "%d (%0.1f per day)" tnum30 txnrate30)+        ,("Transactions last 7 days", printf "%d (%0.1f per day)" tnum7 txnrate7)+--        ,("Payees/descriptions", show $ length $ nub $ map tdescription ts)+        ,("Accounts", show $ length $ accounts l)+        ,("Account tree depth", show $ maximum $ map (accountNameLevel.aname) $ accounts l)+        ,("Commodities", printf "%s (%s)" (show $ length $ cs) (intercalate ", " $ sort $ map symbol cs)) +      -- Transactions this month     : %(monthtxns)s (last month in the same period: %(lastmonthtxns)s)+      -- Uncleared transactions      : %(uncleared)s+      -- Days since reconciliation   : %(reconcileelapsed)s+      -- Days since last transaction : %(recentelapsed)s+       ]+           where+             ts = sortBy (comparing tdate) $ jtxns $ journal l+             lastdate | null ts = Nothing+                      | otherwise = Just $ tdate $ last ts+             lastelapsed = maybe Nothing (Just . diffDays today) lastdate+             showelapsed Nothing = ""+             showelapsed (Just days) = printf " (%d %s)" days' direction+                                       where days' = abs days+                                             direction | days >= 0 = "days ago"+                                                       | otherwise = "days from now"+             tnum = length ts+             span = rawdatespan l+             start (DateSpan (Just d) _) = show d+             start _ = ""+             end (DateSpan _ (Just d)) = show d+             end _ = ""+             days = fromMaybe 0 $ daysInSpan span+             txnrate | days==0 = 0+                     | otherwise = fromIntegral tnum / fromIntegral days :: Double+             tnum30 = length $ filter withinlast30 ts+             withinlast30 t = d >= addDays (-30) today && (d<=today) where d = tdate t+             txnrate30 = fromIntegral tnum30 / 30 :: Double+             tnum7 = length $ filter withinlast7 ts+             withinlast7 t = d >= addDays (-7) today && (d<=today) where d = tdate t+             txnrate7 = fromIntegral tnum7 / 7 :: Double+             cs = Map.elems $ commodities l+
+ Hledger/Cli/Commands/Vty.hs view
@@ -0,0 +1,379 @@+{-| ++A simple text UI for hledger, based on the vty library.++-}++module Hledger.Cli.Commands.Vty+where+import Safe (headDef)+import Graphics.Vty+import Hledger.Data+import Hledger.Cli.Options+import Hledger.Cli.Commands.Balance+import Hledger.Cli.Commands.Register+import Hledger.Cli.Commands.Print+++helpmsg = "(b)alance, (r)egister, (p)rint, (right) to drill down, (left) to back up, (q)uit"++instance Show Vty where show = const "a Vty"++-- | The application state when running the vty command.+data AppState = AppState {+     av :: Vty                   -- ^ the vty context+    ,aw :: Int                  -- ^ window width+    ,ah :: Int                  -- ^ window height+    ,amsg :: String              -- ^ status message+    ,aopts :: [Opt]              -- ^ command-line opts+    ,aargs :: [String]           -- ^ command-line args at startup+    ,ajournal :: Journal         -- ^ parsed journal+    ,abuf :: [String]            -- ^ lines of the current buffered view+    ,alocs :: [Loc]              -- ^ user's navigation trail within the UI+                                -- ^ never null, head is current location+    } deriving (Show)++-- | A location within the user interface.+data Loc = Loc {+     scr :: Screen               -- ^ one of the available screens+    ,sy :: Int                   -- ^ viewport y scroll position+    ,cy :: Int                   -- ^ cursor y position+    ,largs :: [String]           -- ^ command-line args, possibly narrowed for this location+    } deriving (Show)++-- | The screens available within the user interface.+data Screen = BalanceScreen     -- ^ like hledger balance, shows accounts+            | RegisterScreen    -- ^ like hledger register, shows transaction-postings+            | PrintScreen       -- ^ like hledger print, shows ledger transactions+            -- | LedgerScreen      -- ^ shows the raw ledger+              deriving (Eq,Show)++-- | Run the vty (curses-style) ui.+vty :: [Opt] -> [String] -> Journal -> IO ()+vty opts args j = do+  v <- mkVty+  DisplayRegion w h <- display_bounds $ terminal v+  let opts' = SubTotal:opts+  t <-  getCurrentLocalTime+  let a = enter t BalanceScreen args+          AppState {+                  av=v+                 ,aw=fromIntegral w+                 ,ah=fromIntegral h+                 ,amsg=helpmsg+                 ,aopts=opts'+                 ,aargs=args+                 ,ajournal=j+                 ,abuf=[]+                 ,alocs=[]+                 }+  go a ++-- | Update the screen, wait for the next event, repeat.+go :: AppState -> IO ()+go a@AppState{av=av,aopts=opts} = do+  when (notElem DebugVty opts) $ update av (renderScreen a)+  k <- next_event av+  t <- getCurrentLocalTime+  case k of +    EvResize x y                -> go $ resize x y a+    EvKey (KASCII 'l') [MCtrl]  -> refresh av >> go a{amsg=helpmsg}+    EvKey (KASCII 'b') []       -> go $ resetTrailAndEnter t BalanceScreen a+    EvKey (KASCII 'r') []       -> go $ resetTrailAndEnter t RegisterScreen a+    EvKey (KASCII 'p') []       -> go $ resetTrailAndEnter t PrintScreen a+    EvKey KRight []             -> go $ drilldown t a+    EvKey KEnter []             -> go $ drilldown t a+    EvKey KLeft  []             -> go $ backout t a+    EvKey KUp    []             -> go $ moveUpAndPushEdge a+    EvKey KDown  []             -> go $ moveDownAndPushEdge a+    EvKey KHome  []             -> go $ moveToTop a+    EvKey KUp    [MCtrl]        -> go $ moveToTop a+    EvKey KUp    [MShift]       -> go $ moveToTop a+    EvKey KEnd   []             -> go $ moveToBottom a+    EvKey KDown  [MCtrl]        -> go $ moveToBottom a+    EvKey KDown  [MShift]       -> go $ moveToBottom a+    EvKey KPageUp []            -> go $ prevpage a+    EvKey KBS []                -> go $ prevpage a+    EvKey (KASCII ' ') [MShift] -> go $ prevpage a+    EvKey KPageDown []          -> go $ nextpage a+    EvKey (KASCII ' ') []       -> go $ nextpage a+    EvKey (KASCII 'q') []       -> shutdown av >> return ()+--    EvKey KEsc   []           -> shutdown av >> return ()+    _                           -> go a++-- app state modifiers++-- | The number of lines currently available for the main data display area.+pageHeight :: AppState -> Int+pageHeight a = ah a - 1++setLocCursorY, setLocScrollY :: Int -> Loc -> Loc+setLocCursorY y l = l{cy=y}+setLocScrollY y l = l{sy=y}++cursorY, scrollY, posY :: AppState -> Int+cursorY = cy . loc+scrollY = sy . loc+posY a = scrollY a + cursorY a++setCursorY, setScrollY, setPosY :: Int -> AppState -> AppState+setCursorY _ AppState{alocs=[]} = error "shouldn't happen" -- silence warnings+setCursorY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)} where l' = setLocCursorY y l++setScrollY _ AppState{alocs=[]} = error "shouldn't happen" -- silence warnings+setScrollY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)} where l' = setLocScrollY y l++setPosY _ AppState{alocs=[]}    = error "shouldn't happen" -- silence warnings+setPosY y a@AppState{alocs=(l:locs)} = a{alocs=(l':locs)}+    where +      l' = setLocScrollY sy $ setLocCursorY cy l+      ph = pageHeight a+      cy = y `mod` ph+      sy = y - cy++updateCursorY, updateScrollY, updatePosY :: (Int -> Int) -> AppState -> AppState+updateCursorY f a = setCursorY (f $ cursorY a) a+updateScrollY f a = setScrollY (f $ scrollY a) a+updatePosY f a = setPosY (f $ posY a) a++resize :: Int -> Int -> AppState -> AppState+resize x y a = setCursorY cy' a{aw=x,ah=y}+    where+      cy = cursorY a+      cy' = min cy (y-2)++moveToTop :: AppState -> AppState+moveToTop = setPosY 0++moveToBottom :: AppState -> AppState+moveToBottom a = setPosY (length $ abuf a) a++moveUpAndPushEdge :: AppState -> AppState+moveUpAndPushEdge a+    | cy > 0 = updateCursorY (subtract 1) a+    | sy > 0 = updateScrollY (subtract 1) a+    | otherwise = a+    where Loc{sy=sy,cy=cy} = head $ alocs a++moveDownAndPushEdge :: AppState -> AppState+moveDownAndPushEdge a+    | sy+cy >= bh = a+    | cy < ph-1 = updateCursorY (+1) a+    | otherwise = updateScrollY (+1) a+    where +      Loc{sy=sy,cy=cy} = head $ alocs a+      ph = pageHeight a+      bh = length $ abuf a++-- | Scroll down by page height or until we can just see the last line,+-- without moving the cursor, or if we are already scrolled as far as+-- possible then move the cursor to the last line.+nextpage :: AppState -> AppState+nextpage (a@AppState{abuf=b})+    | sy < bh-jump = setScrollY sy' a+    | otherwise    = setCursorY (bh-sy) a+    where+      sy = scrollY a+      jump = pageHeight a - 1+      bh = length b+      sy' = min (sy+jump) (bh-jump)++-- | Scroll up by page height or until we can just see the first line,+-- without moving the cursor, or if we are scrolled as far as possible+-- then move the cursor to the first line.+prevpage :: AppState -> AppState+prevpage a+    | sy > 0    = setScrollY sy' a+    | otherwise = setCursorY 0 a+    where+      sy = scrollY a+      jump = pageHeight a - 1+      sy' = max (sy-jump) 0++-- | Push a new UI location on to the stack.+pushLoc :: Loc -> AppState -> AppState+pushLoc l a = a{alocs=(l:alocs a)}++popLoc :: AppState -> AppState+popLoc a@AppState{alocs=locs}+    | length locs > 1 = a{alocs=drop 1 locs}+    | otherwise = a++clearLocs :: AppState -> AppState+clearLocs a = a{alocs=[]}++exit :: AppState -> AppState +exit = popLoc++loc :: AppState -> Loc+loc = head . alocs++-- | Get the filter pattern args in effect for the current ui location.+currentArgs :: AppState -> [String]+currentArgs (AppState {alocs=Loc{largs=as}:_}) = as+currentArgs (AppState {aargs=as}) = as++screen :: AppState -> Screen+screen a = scr where (Loc scr _ _ _) = loc a++-- | Enter a new screen, with possibly new args, adding the new ui location to the stack.+enter :: LocalTime -> Screen -> [String] -> AppState -> AppState+enter t scr@BalanceScreen args a  = updateData t $ pushLoc Loc{scr=scr,sy=0,cy=0,largs=args} a+enter t scr@RegisterScreen args a = updateData t $ pushLoc Loc{scr=scr,sy=0,cy=0,largs=args} a+enter t scr@PrintScreen args a    = updateData t $ pushLoc Loc{scr=scr,sy=0,cy=0,largs=args} a++resetTrailAndEnter :: LocalTime -> Screen -> AppState -> AppState+resetTrailAndEnter t scr a = enter t scr (aargs a) $ clearLocs a++-- | Regenerate the display data appropriate for the current screen.+updateData :: LocalTime -> AppState -> AppState+updateData t a@AppState{aopts=opts,ajournal=j} =+    case screen a of+      BalanceScreen  -> a{abuf=lines $ showBalanceReport opts fspec j}+      RegisterScreen -> a{abuf=lines $ showRegisterReport opts fspec j}+      PrintScreen    -> a{abuf=lines $ showTransactions fspec j}+    where fspec = optsToFilterSpec opts (currentArgs a) t++backout :: LocalTime -> AppState -> AppState+backout t a | screen a == BalanceScreen = a+            | otherwise = updateData t $ popLoc a++drilldown :: LocalTime -> AppState -> AppState+drilldown t a =+    case screen a of+      BalanceScreen  -> enter t RegisterScreen [currentAccountName a] a+      RegisterScreen -> scrollToTransaction e $ enter t PrintScreen (currentArgs a) a+      PrintScreen   -> a+    where e = currentTransaction a++-- | Get the account name currently highlighted by the cursor on the+-- balance screen. Results undefined while on other screens.+currentAccountName :: AppState -> AccountName+currentAccountName a = accountNameAt (abuf a) (posY a)++-- | Get the full name of the account being displayed at a specific line+-- within the balance command's output.+accountNameAt :: [String] -> Int -> AccountName+accountNameAt buf lineno = accountNameFromComponents anamecomponents+    where+      namestohere = map (drop 22) $ take (lineno+1) buf+      (indented, nonindented) = span (" " `isPrefixOf`) $ reverse namestohere+      thisbranch = indented ++ take 1 nonindented+      anamecomponents = reverse $ map strip $ dropsiblings thisbranch+      dropsiblings :: [AccountName] -> [AccountName]+      dropsiblings [] = []+      dropsiblings (x:xs) = x : dropsiblings xs'+          where+            xs' = dropWhile moreindented xs+            moreindented = (>= myindent) . indentof+            myindent = indentof x+            indentof = length . takeWhile (==' ')++-- | If on the print screen, move the cursor to highlight the specified entry+-- (or a reasonable guess). Doesn't work.+scrollToTransaction :: Maybe Transaction -> AppState -> AppState+scrollToTransaction Nothing a = a+scrollToTransaction (Just t) a@AppState{abuf=buf} = setCursorY cy $ setScrollY sy a+    where+      entryfirstline = head $ lines $ showTransaction t+      halfph = pageHeight a `div` 2+      y = fromMaybe 0 $ findIndex (== entryfirstline) buf+      sy = max 0 $ y - halfph+      cy = y - sy++-- | Get the transaction containing the posting currently highlighted by+-- the cursor on the register screen (or best guess). Results undefined+-- while on other screens.+currentTransaction :: AppState -> Maybe Transaction+currentTransaction a@AppState{ajournal=j,abuf=buf} = ptransaction p+    where+      p = headDef nullposting $ filter ismatch $ journalPostings j+      ismatch p = postingDate p == parsedate (take 10 datedesc)+                  && take 70 (showPostingWithBalance False p nullmixedamt) == (datedesc ++ acctamt)+      datedesc = take 32 $ fromMaybe "" $ find (not . (" " `isPrefixOf`)) $ headDef "" rest : reverse above+      acctamt = drop 32 $ headDef "" rest+      (above,rest) = splitAt y buf+      y = posY a++-- renderers++renderScreen :: AppState -> Picture+renderScreen (a@AppState{aw=w,ah=h,abuf=buf,amsg=msg}) =+    Picture {pic_cursor = Cursor (fromIntegral cx) (fromIntegral cy)+            ,pic_image = mainimg+                         <->+                         renderStatus w msg+            ,pic_background = Background ' ' def_attr+            }+    where +      (cx, cy) = (0, cursorY a)+      sy = scrollY a+--       mainimg = (renderString attr $ unlines $ above)+--           <->+--           (renderString reverseattr $ thisline)+--           <->+--           (renderString attr $ unlines $ below)+--       (above,(thisline:below))+--           | null ls   = ([],[""])+--           | otherwise = splitAt y ls+--       ls = lines $ fitto w (h-1) $ unlines $ drop as $ buf+-- trying for more speed+      mainimg = vert_cat (map (string defaultattr) above)+               <->+               string currentlineattr thisline+               <->+               vert_cat (map (string defaultattr) below)+      (thisline,below) | null rest = (blankline,[])+                       | otherwise = (head rest, tail rest)+      (above,rest) = splitAt cy linestorender+      linestorender = map padclipline $ take (h-1) $ drop sy $ buf ++ replicate h blankline+      padclipline = take w . (++ blankline)+      blankline = replicate w ' '++padClipString :: Int -> Int -> String -> [String]+padClipString h w s = rows+    where+      rows = map padclipline $ take h $ lines s ++ replicate h blankline+      padclipline = take w . (++ blankline)+      blankline = replicate w ' '++renderString :: Attr -> String -> Image+renderString attr s = vert_cat $ map (string attr) rows+    where+      rows = lines $ fitto w h s+      w = maximum $ map length ls+      h = length ls+      ls = lines s++renderStatus :: Int -> String -> Image+renderStatus w = string statusattr . take w . (++ repeat ' ')++-- the all-important theming engine!++theme = Restrained++data UITheme = Restrained | Colorful | Blood++(defaultattr, + currentlineattr, + statusattr+ ) = case theme of+       Restrained -> (def_attr+                    ,def_attr `with_style` bold+                    ,def_attr `with_style` reverse_video+                    )+       Colorful   -> (def_attr `with_style` reverse_video+                    ,def_attr `with_fore_color` white `with_back_color` red+                    ,def_attr `with_fore_color` black `with_back_color` green+                    )+       Blood      -> (def_attr `with_style` reverse_video+                    ,def_attr `with_fore_color` white `with_back_color` red+                    ,def_attr `with_style` reverse_video+                    )++halfbrightattr = def_attr `with_style` dim+reverseattr = def_attr `with_style` reverse_video+redattr = def_attr `with_fore_color` red+greenattr = def_attr `with_fore_color` green+reverseredattr = def_attr `with_style` reverse_video `with_fore_color` red+reversegreenattr= def_attr `with_style` reverse_video `with_fore_color` green
+ Hledger/Cli/Commands/Web.hs view
@@ -0,0 +1,345 @@+{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, MultiParamTypeClasses #-}+{-# OPTIONS_GHC -F -pgmFtrhsx #-}+{-| +A web-based UI.+-}++module Hledger.Cli.Commands.Web+where+import Codec.Binary.UTF8.String (decodeString)+import Control.Applicative.Error (Failing(Success,Failure))+import Control.Concurrent+import Control.Monad.Reader (ask)+import Data.IORef (newIORef, atomicModifyIORef)+import System.Directory (getModificationTime)+import System.IO.Storage (withStore, putValue, getValue)+import System.Time (ClockTime, getClockTime, diffClockTimes, TimeDiff(TimeDiff))+import Text.ParserCombinators.Parsec (parse)++import Hack.Contrib.Constants (_TextHtmlUTF8)+import Hack.Contrib.Response (set_content_type)+import qualified Hack (Env, http)+import qualified Hack.Contrib.Request (inputs, params, path)+import qualified Hack.Contrib.Response (redirect)+#ifdef WEBHAPPSTACK+import System.Process (readProcess)+import Hack.Handler.Happstack (runWithConfig,ServerConf(ServerConf))+#else+import Hack.Handler.SimpleServer (run)+#endif++import Network.Loli (loli, io, get, post, html, text, public)+import Network.Loli.Type (AppUnit)+import Network.Loli.Utils (update)++import HSP hiding (Request,catch)+import qualified HSP (Request(..))++import Hledger.Cli.Commands.Add (journalAddTransaction)+import Hledger.Cli.Commands.Balance+import Hledger.Cli.Commands.Histogram+import Hledger.Cli.Commands.Print+import Hledger.Cli.Commands.Register+import Hledger.Data+import Hledger.Cli.Options hiding (value)+#ifdef MAKE+import Paths_hledger_make (getDataFileName)+#else+import Paths_hledger (getDataFileName)+#endif+import Hledger.Cli.Utils (openBrowserOn)+++tcpport = 5000 :: Int+homeurl = printf "http://localhost:%d/" tcpport+browserdelay = 100000 -- microseconds++web :: [Opt] -> [String] -> Journal -> IO ()+web opts args j = do+  unless (Debug `elem` opts) $ forkIO browser >> return ()+  server opts args j++browser :: IO ()+browser = putStrLn "starting web browser" >> threadDelay browserdelay >> openBrowserOn homeurl >> return ()++server :: [Opt] -> [String] -> Journal -> IO ()+server opts args j =+  -- server initialisation+  withStore "hledger" $ do -- IO ()+    printf "starting web server on port %d\n" tcpport+    t <- getCurrentLocalTime+    webfiles <- getDataFileName "web"+    putValue "hledger" "journal" j+#ifdef WEBHAPPSTACK+    hostname <- readProcess "hostname" [] "" `catch` \_ -> return "hostname"+    runWithConfig (ServerConf tcpport hostname) $            -- (Env -> IO Response) -> IO ()+#else+    run tcpport $            -- (Env -> IO Response) -> IO ()+#endif+      \env -> do -- IO Response+       -- general request handler+       let opts' = opts ++ [Period $ unwords $ map decodeString $ reqParamUtf8 env "p"]+           args' = args ++ map decodeString (reqParamUtf8 env "a")+       j' <- fromJust `fmap` getValue "hledger" "journal"+       j'' <- journalReloadIfChanged opts' args' j'+       -- declare path-specific request handlers+       let command :: [String] -> ([Opt] -> FilterSpec -> Journal -> String) -> AppUnit+           command msgs f = string msgs $ f opts' (optsToFilterSpec opts' args' t) j''+       (loli $                                               -- State Loli () -> (Env -> IO Response)+         do+          get  "/balance"   $ command [] showBalanceReport  -- String -> ReaderT Env (StateT Response IO) () -> State Loli ()+          get  "/register"  $ command [] showRegisterReport+          get  "/histogram" $ command [] showHistogram+          get  "/transactions"   $ ledgerpage [] j'' (showTransactions (optsToFilterSpec opts' args' t))+          post "/transactions"   $ handleAddform j''+          get  "/env"       $ getenv >>= (text . show)+          get  "/params"    $ getenv >>= (text . show . Hack.Contrib.Request.params)+          get  "/inputs"    $ getenv >>= (text . show . Hack.Contrib.Request.inputs)+          public (Just webfiles) ["/style.css"]+          get  "/"          $ redirect ("transactions") Nothing+          ) env++getenv = ask+response = update+redirect u c = response $ Hack.Contrib.Response.redirect u c++reqParamUtf8 :: Hack.Env -> String -> [String]+reqParamUtf8 env p = map snd $ filter ((==p).fst) $ Hack.Contrib.Request.params env++journalReloadIfChanged :: [Opt] -> [String] -> Journal -> IO Journal+journalReloadIfChanged opts _ j@Journal{filepath=f,filereadtime=tread} = do+  tmod <- journalFileModifiedTime j+  let newer = diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)+  -- when (Debug `elem` opts) $ printf "checking file, last modified %s, last read %s, %s\n" (show tmod) (show tread) (show newer)+  if newer+   then do+     when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" f+     reload j+   else return j++journalFileModifiedTime :: Journal -> IO ClockTime+journalFileModifiedTime Journal{filepath=f}+    | null f = getClockTime+    | otherwise = getModificationTime f `Prelude.catch` \_ -> getClockTime++reload :: Journal -> IO Journal+reload Journal{filepath=f} = do+  j' <- readJournal f+  putValue "hledger" "journal" j'+  return j'+            +ledgerpage :: [String] -> Journal -> (Journal -> String) -> AppUnit+ledgerpage msgs j f = do+  env <- getenv+  j' <- io $ journalReloadIfChanged [] [] j+  hsp msgs $ const <div><% addform env %><pre><% f j' %></pre></div>++-- | A loli directive to serve a string in pre tags within the hledger web+-- layout.+string :: [String] -> String -> AppUnit+string msgs s = hsp msgs $ const <pre><% s %></pre>++-- | A loli directive to serve a hsp template wrapped in the hledger web+-- layout. The hack environment is passed in to every hsp template as an+-- argument, since I don't see how to get it within the hsp monad.+-- A list of messages is also passed, eg for form errors.+hsp :: [String] -> (Hack.Env -> HSP XML) -> AppUnit+hsp msgs f = do+  env <- getenv+  let contenthsp = f env+      pagehsp = hledgerpage env msgs title contenthsp+  html =<< (io $ do+              hspenv <- hackEnvToHspEnv env+              (_,xml) <- runHSP html4Strict pagehsp hspenv+              return $ addDoctype $ renderAsHTML xml)+  response $ set_content_type _TextHtmlUTF8+    where+      title = ""+      addDoctype = ("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n" ++)+      hackEnvToHspEnv :: Hack.Env -> IO HSPEnv+      hackEnvToHspEnv env = do+          x <- newIORef 0+          let req = HSP.Request (reqParamUtf8 env) (Hack.http env)+              num = NumberGen (atomicModifyIORef x (\a -> (a+1,a)))+          return $ HSPEnv req num++-- htmlToHsp :: Html -> HSP XML+-- htmlToHsp h = return $ cdata $ showHtml h++-- views++hledgerpage :: Hack.Env -> [String] -> String -> HSP XML -> HSP XML+hledgerpage env msgs title content =+    <html>+      <head>+        <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />+        <link rel="stylesheet" type="text/css" href="/style.css" media="all" />+        <title><% title %></title>+      </head>+      <body>+        <% navbar env %>+        <div id="messages"><% intercalate ", " msgs %></div>+        <div id="content"><% content %></div>+      </body>+    </html>++navbar :: Hack.Env -> HSP XML+navbar env =+    <div id="navbar">+      <a href="http://hledger.org" id="hledgerorglink">hledger.org</a>+      <% navlinks env %>+      <% searchform env %>+      <a href="http://hledger.org/MANUAL.html" id="helplink">help</a>+    </div>++getParamOrNull p = (decodeString . fromMaybe "") `fmap` getParam p++navlinks :: Hack.Env -> HSP XML+navlinks _ = do+   a <- getParamOrNull "a"+   p <- getParamOrNull "p"+   let addparams=(++(printf "?a=%s&p=%s" a p))+       link s = <a href=(addparams s) class="navlink"><% s %></a>+   <div id="navlinks">+     <% link "transactions" %> |+     <% link "register" %> |+     <% link "balance" %>+    </div>++searchform :: Hack.Env -> HSP XML+searchform env = do+   a <- getParamOrNull "a"+   p <- getParamOrNull "p"+   let resetlink | null a && null p = <span></span>+                 | otherwise = <span id="resetlink"><% nbsp %><a href=u>reset</a></span>+                 where u = dropWhile (=='/') $ Hack.Contrib.Request.path env+   <form action="" id="searchform">+      <% nbsp %>search for:<% nbsp %><input name="a" size="20" value=a+      /><% help "filter-patterns"+      %><% nbsp %><% nbsp %>in reporting period:<% nbsp %><input name="p" size="20" value=p+      /><% help "period-expressions"+      %><input type="submit" name="submit" value="filter" style="display:none" />+      <% resetlink %>+    </form>++addform :: Hack.Env -> HSP XML+addform env = do+  today <- io $ liftM showDate $ getCurrentDay+  let inputs = Hack.Contrib.Request.inputs env+      date  = decodeString $ fromMaybe today $ lookup "date"  inputs+      desc  = decodeString $ fromMaybe "" $ lookup "desc"  inputs+  <div>+   <div id="addform">+   <form action="" method="POST">+    <table border="0">+      <tr>+        <td>+          Date: <input size="15" name="date" value=date /><% help "dates" %><% nbsp %>+          Description: <input size="35" name="desc" value=desc /><% nbsp %>+        </td>+      </tr>+      <% transactionfields 1 env %>+      <% transactionfields 2 env %>+      <tr id="addbuttonrow"><td><input type="submit" value="add transaction" +      /><% help "file-format" %></td></tr>+    </table>+   </form>+   </div>+   <br clear="all" />+   </div>++help :: String -> HSP XML+help topic = <a href=u>?</a>+    where u = printf "http://hledger.org/MANUAL.html%s" l :: String+          l | null topic = ""+            | otherwise = '#':topic++transactionfields :: Int -> Hack.Env -> HSP XML+transactionfields n env = do+  let inputs = Hack.Contrib.Request.inputs env+      acct = decodeString $ fromMaybe "" $ lookup acctvar inputs+      amt  = decodeString $ fromMaybe "" $ lookup amtvar  inputs+  <tr>+    <td>+    <% nbsp %><% nbsp %>+      Account: <input size="35" name=acctvar value=acct /><% nbsp %>+      Amount: <input size="15" name=amtvar value=amt /><% nbsp %>+    </td>+   </tr>+    where+      numbered = (++ show n)+      acctvar = numbered "acct"+      amtvar = numbered "amt"++handleAddform :: Journal -> AppUnit+handleAddform j = do+  env <- getenv+  d <- io getCurrentDay+  t <- io getCurrentLocalTime+  handle t $ validate env d+  where+    validate :: Hack.Env -> Day -> Failing Transaction+    validate env today =+        let inputs = Hack.Contrib.Request.inputs env+            date  = decodeString $ fromMaybe "today" $ lookup "date"  inputs+            desc  = decodeString $ fromMaybe "" $ lookup "desc"  inputs+            acct1 = decodeString $ fromMaybe "" $ lookup "acct1" inputs+            amt1  = decodeString $ fromMaybe "" $ lookup "amt1"  inputs+            acct2 = decodeString $ fromMaybe "" $ lookup "acct2" inputs+            amt2  = decodeString $ fromMaybe "" $ lookup "amt2"  inputs+            validateDate ""  = ["missing date"]+            validateDate _   = []+            validateDesc ""  = ["missing description"]+            validateDesc _   = []+            validateAcct1 "" = ["missing account 1"]+            validateAcct1 _  = []+            validateAmt1 ""  = ["missing amount 1"]+            validateAmt1 _   = []+            validateAcct2 "" = ["missing account 2"]+            validateAcct2 _  = []+            validateAmt2 _   = []+            amt1' = either (const missingamt) id $ parse someamount "" amt1+            amt2' = either (const missingamt) id $ parse someamount "" amt2+            (date', dateparseerr) = case fixSmartDateStrEither today date of+                                      Right d -> (d, [])+                                      Left e -> ("1900/01/01", [showDateParseError e])+            t = Transaction {+                            tdate = parsedate date' -- date' must be parseable+                           ,teffectivedate=Nothing+                           ,tstatus=False+                           ,tcode=""+                           ,tdescription=desc+                           ,tcomment=""+                           ,tpostings=[+                             Posting False acct1 amt1' "" RegularPosting (Just t')+                            ,Posting False acct2 amt2' "" RegularPosting (Just t')+                            ]+                           ,tpreceding_comment_lines=""+                           }+            (t', balanceerr) = case balanceTransaction t of+                           Right t'' -> (t'', [])+                           Left e -> (t, [head $ lines e]) -- show just the error not the transaction+            errs = concat [+                    validateDate date+                   ,dateparseerr+                   ,validateDesc desc+                   ,validateAcct1 acct1+                   ,validateAmt1 amt1+                   ,validateAcct2 acct2+                   ,validateAmt2 amt2+                   ,balanceerr+                   ]+        in+        case null errs of+          False -> Failure errs+          True  -> Success t'++    handle :: LocalTime -> Failing Transaction -> AppUnit+    handle _ (Failure errs) = hsp errs addform+    handle ti (Success t)   = do+                    io $ journalAddTransaction j t >> reload j+                    ledgerpage [msg] j (showTransactions (optsToFilterSpec [] [] ti))+       where msg = printf "Added transaction:\n%s" (show t)++nbsp :: XML+nbsp = cdata "&nbsp;"
+ Hledger/Cli/Main.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE CPP #-}+{-|+hledger - a ledger-compatible accounting tool.+Copyright (c) 2007-2010 Simon Michael <simon@joyful.com>+Released under GPL version 3 or later.++hledger is a partial haskell clone of John Wiegley's "ledger".  It+generates ledger-compatible register & balance reports from a plain text+journal, and demonstrates a functional implementation of ledger.+For more information, see http:\/\/hledger.org .++This module provides the main function for the hledger command-line+executable. It is exposed here so that it can be imported by eg benchmark+scripts.++You can use the command line:++> $ hledger --help++or ghci:++> $ ghci hledger+> > j <- readJournal "data/sample.journal"+> > register [] ["income","expenses"] j+> 2008/01/01 income               income:salary                   $-1          $-1+> 2008/06/01 gift                 income:gifts                    $-1          $-2+> 2008/06/03 eat & shop           expenses:food                    $1          $-1+>                                 expenses:supplies                $1            0+> > balance [Depth "1"] [] l+>                  $-1  assets+>                   $2  expenses+>                  $-2  income+>                   $1  liabilities+> > l <- myLedger+> > t <- myTimelog++See "Hledger.Data.Ledger" for more examples.++-}++module Hledger.Cli.Main where+#if __GLASGOW_HASKELL__ <= 610+import Prelude hiding (putStr, putStrLn)+import System.IO.UTF8+#endif++import Hledger.Cli.Commands.All+import Hledger.Data+import Hledger.Cli.Options+import Hledger.Cli.Tests+import Hledger.Cli.Utils (withJournalDo)+import Hledger.Cli.Version (versionmsg, binaryfilename)++main :: IO ()+main = do+  (opts, cmd, args) <- parseArguments+  run cmd opts args+    where+      run cmd opts args+       | Help `elem` opts             = putStr usage+       | Version `elem` opts          = putStrLn versionmsg+       | BinaryFilename `elem` opts   = putStrLn binaryfilename+       | cmd `isPrefixOf` "balance"   = withJournalDo opts args cmd balance+       | cmd `isPrefixOf` "convert"   = withJournalDo opts args cmd convert+       | cmd `isPrefixOf` "print"     = withJournalDo opts args cmd print'+       | cmd `isPrefixOf` "register"  = withJournalDo opts args cmd register+       | cmd `isPrefixOf` "histogram" = withJournalDo opts args cmd histogram+       | cmd `isPrefixOf` "add"       = withJournalDo opts args cmd add+       | cmd `isPrefixOf` "stats"     = withJournalDo opts args cmd stats+#ifdef VTY+       | cmd `isPrefixOf` "vty"       = withJournalDo opts args cmd vty+#endif+#if defined(WEB) || defined(WEBHAPPSTACK)+       | cmd `isPrefixOf` "web"       = withJournalDo opts args cmd web+#endif+#ifdef CHART+       | cmd `isPrefixOf` "chart"       = withJournalDo opts args cmd chart+#endif+       | cmd `isPrefixOf` "test"      = runtests opts args >> return ()+       | otherwise                    = putStr usage
+ Hledger/Cli/Options.hs view
@@ -0,0 +1,269 @@+{-# LANGUAGE CPP #-}+{-|+Command-line options for the application.+-}++module Hledger.Cli.Options+where+import System.Console.GetOpt+import System.Environment+import Hledger.Cli.Version (timeprogname)+import Hledger.Data.IO (myLedgerPath,myTimelogPath)+import Hledger.Data.Utils+import Hledger.Data.Types+import Hledger.Data.Dates+import Codec.Binary.UTF8.String (decodeString)++#ifdef CHART+chartoutput   = "hledger.png"+chartitems    = 10+chartsize     = "600x400"+#endif++usagehdr =+  "Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]\n" +++  "       hledger [OPTIONS] convert CSVFILE\n" +++  "       hledger [OPTIONS] stats\n" +++  "\n" +++  "hledger uses your ~/.ledger or $LEDGER file, or another specified with -f\n" +++  "\n" +++  "COMMAND is one of (may be abbreviated):\n" +++  "  add       - prompt for new transactions and add them to the ledger\n" +++  "  balance   - show accounts, with balances\n" +++  "  convert   - read CSV bank data and display in ledger format\n" +++  "  histogram - show a barchart of transactions per day or other interval\n" +++  "  print     - show transactions in ledger format\n" +++  "  register  - show transactions as a register with running balance\n" +++  "  stats     - show various statistics for a ledger\n" +++#ifdef VTY+  "  vty       - run a simple curses-style UI\n" +++#endif+#ifdef WEB+  "  web       - run a simple web-based UI\n" +++#endif+#ifdef CHART+  "  chart     - generate balances pie chart\n" +++#endif+  "  test      - run self-tests\n" +++  "\n" +++  "PATTERNS are regular expressions which filter by account name.\n" +++  "Prefix with desc: to filter by transaction description instead.\n" +++  "Prefix with not: to negate a pattern. When using both, not: comes last.\n" +++  "\n" +++  "DATES can be y/m/d or ledger-style smart dates like \"last month\".\n" +++  "\n" +++  "Options:"++usageftr = ""+usage = usageInfo usagehdr options ++ usageftr++-- | Command-line options we accept.+options :: [OptDescr Opt]+options = [+  Option "f" ["file"]         (ReqArg File "FILE")   "use a different ledger/timelog file; - means stdin"+ ,Option ""  ["no-new-accounts"] (NoArg NoNewAccts)   "don't allow to create new accounts"+ ,Option "b" ["begin"]        (ReqArg Begin "DATE")  "report on transactions on or after this date"+ ,Option "e" ["end"]          (ReqArg End "DATE")    "report on transactions before this date"+ ,Option "p" ["period"]       (ReqArg Period "EXPR") ("report on transactions during the specified period\n" +++                                                       "and/or with the specified reporting interval\n")+ ,Option "C" ["cleared"]      (NoArg  Cleared)       "report only on cleared transactions"+ ,Option "U" ["uncleared"]    (NoArg  UnCleared)     "report only on uncleared transactions"+ ,Option "B" ["cost","basis"] (NoArg  CostBasis)     "report cost of commodities"+ ,Option ""    ["depth"]        (ReqArg Depth "N")     "hide accounts/transactions deeper than this"+ ,Option "d" ["display"]      (ReqArg Display "EXPR") ("show only transactions matching EXPR (where\n" +++                                                        "EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)")+ ,Option ""    ["effective"]    (NoArg  Effective)     "use transactions' effective dates, if any"+ ,Option "E" ["empty"]        (NoArg  Empty)         "show empty/zero things which are normally elided"+ ,Option "R" ["real"]         (NoArg  Real)          "report only on real (non-virtual) transactions"+ ,Option ""    ["no-total"]     (NoArg  NoTotal)       "balance report: hide the final total"+-- ,Option "s" ["subtotal"]     (NoArg  SubTotal)      "balance report: show subaccounts"+ ,Option "W" ["weekly"]       (NoArg  WeeklyOpt)     "register report: show weekly summary"+ ,Option "M" ["monthly"]      (NoArg  MonthlyOpt)    "register report: show monthly summary"+ ,Option "Q" ["quarterly"]    (NoArg  QuarterlyOpt)  "register report: show quarterly summary"+ ,Option "Y" ["yearly"]       (NoArg  YearlyOpt)     "register report: show yearly summary"+ ,Option "h" ["help"] (NoArg  Help)                  "show this help"+ ,Option "V" ["version"]      (NoArg  Version)       "show version information"+ ,Option "v" ["verbose"]      (NoArg  Verbose)       "show verbose test output"+ ,Option ""    ["binary-filename"] (NoArg BinaryFilename) "show the download filename for this hledger build"+ ,Option ""    ["debug"]        (NoArg  Debug)         "show extra debug output; implies verbose"+ ,Option ""    ["debug-vty"]  (NoArg  DebugVty)     "run vty command with no vty output, showing console"+#ifdef CHART+ ,Option "o" ["output"]  (ReqArg ChartOutput "FILE")    ("chart: output filename (default: "++chartoutput++")")+ ,Option ""  ["items"]  (ReqArg ChartItems "N")         ("chart: number of accounts to show (default: "++show chartitems++")")+ ,Option ""  ["size"] (ReqArg ChartSize "WIDTHxHEIGHT") ("chart: image size (default: "++chartsize++")")+#endif+ ]++-- | An option value from a command-line flag.+data Opt = +    File    {value::String} | +    NoNewAccts |+    Begin   {value::String} | +    End     {value::String} | +    Period  {value::String} | +    Cleared | +    UnCleared | +    CostBasis | +    Depth   {value::String} | +    Display {value::String} | +    Effective | +    Empty | +    Real | +    NoTotal |+    SubTotal |+    WeeklyOpt |+    MonthlyOpt |+    QuarterlyOpt |+    YearlyOpt |+    Help |+    Verbose |+    Version+    | BinaryFilename+    | Debug+    | DebugVty+#ifdef CHART+    | ChartOutput {value::String}+    | ChartItems  {value::String}+    | ChartSize   {value::String}+#endif+    deriving (Show,Eq)++-- these make me nervous+optsWithConstructor f opts = concatMap get opts+    where get o = [o | f v == o] where v = value o++optsWithConstructors fs opts = concatMap get opts+    where get o = [o | any (== o) fs]++optValuesForConstructor f opts = concatMap get opts+    where get o = [v | f v == o] where v = value o++optValuesForConstructors fs opts = concatMap get opts+    where get o = [v | any (\f -> f v == o) fs] where v = value o++-- | Parse the command-line arguments into options, command name, and+-- command arguments. Any dates in the options are converted to explicit+-- YYYY/MM/DD format based on the current time.+parseArguments :: IO ([Opt], String, [String])+parseArguments = do+  args <- liftM (map decodeString) getArgs+  let (os,as,es) = getOpt Permute options args+--  istimequery <- usingTimeProgramName+--  let os' = if istimequery then (Period "today"):os else os+  os' <- fixOptDates os+  let os'' = if Debug `elem` os' then Verbose:os' else os'+  case (as,es) of+    (cmd:args,[])   -> return (os'',cmd,args)+    ([],[])         -> return (os'',"",[])+    (_,errs)        -> ioError (userError (concat errs ++ usage))++-- | Convert any fuzzy dates within these option values to explicit ones,+-- based on today's date.+fixOptDates :: [Opt] -> IO [Opt]+fixOptDates opts = do+  d <- getCurrentDay+  return $ map (fixopt d) opts+  where+    fixopt d (Begin s)   = Begin $ fixSmartDateStr d s+    fixopt d (End s)     = End $ fixSmartDateStr d s+    fixopt d (Display s) = -- hacky+        Display $ gsubRegexPRBy "\\[.+?\\]" fixbracketeddatestr s+        where fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]"+    fixopt _ o            = o++-- | Figure out the overall date span we should report on, based on any+-- begin/end/period options provided. If there is a period option, the+-- others are ignored.+dateSpanFromOpts :: Day -> [Opt] -> DateSpan+dateSpanFromOpts refdate opts+    | not $ null popts = snd $ parsePeriodExpr refdate $ last popts+    | otherwise = DateSpan lastb laste+    where+      popts = optValuesForConstructor Period opts+      bopts = optValuesForConstructor Begin opts+      eopts = optValuesForConstructor End opts+      lastb = listtomaybeday bopts+      laste = listtomaybeday eopts+      listtomaybeday vs = if null vs then Nothing else Just $ parse $ last vs+          where parse = parsedate . fixSmartDateStr refdate++-- | Figure out the reporting interval, if any, specified by the options.+-- If there is a period option, the others are ignored.+intervalFromOpts :: [Opt] -> Interval+intervalFromOpts opts =+    case (periodopts, intervalopts) of+      ((p:_), _)            -> fst $ parsePeriodExpr d p where d = parsedate "0001/01/01" -- unused+      (_, (WeeklyOpt:_))    -> Weekly+      (_, (MonthlyOpt:_))   -> Monthly+      (_, (QuarterlyOpt:_)) -> Quarterly+      (_, (YearlyOpt:_))    -> Yearly+      (_, _)                -> NoInterval+    where+      periodopts   = reverse $ optValuesForConstructor Period opts+      intervalopts = reverse $ filter (`elem` [WeeklyOpt,MonthlyOpt,QuarterlyOpt,YearlyOpt]) opts++-- | Get the value of the (last) depth option, if any, otherwise a large number.+depthFromOpts :: [Opt] -> Maybe Int+depthFromOpts opts = listtomaybeint $ optValuesForConstructor Depth opts+    where+      listtomaybeint [] = Nothing+      listtomaybeint vs = Just $ read $ last vs++-- | Get the value of the (last) display option, if any.+displayExprFromOpts :: [Opt] -> Maybe String+displayExprFromOpts opts = listtomaybe $ optValuesForConstructor Display opts+    where+      listtomaybe [] = Nothing+      listtomaybe vs = Just $ last vs++-- | Get a maybe boolean representing the last cleared/uncleared option if any.+clearedValueFromOpts opts | null os = Nothing+                          | last os == Cleared = Just True+                          | otherwise = Just False+    where os = optsWithConstructors [Cleared,UnCleared] opts++-- | Was the program invoked via the \"hours\" alias ?+usingTimeProgramName :: IO Bool+usingTimeProgramName = do+  progname <- getProgName+  return $ map toLower progname == timeprogname++-- | Get the journal file path from options, an environment variable, or a default+journalFilePathFromOpts :: [Opt] -> IO String+journalFilePathFromOpts opts = do+  istimequery <- usingTimeProgramName+  f <- if istimequery then myTimelogPath else myLedgerPath+  return $ last $ f : optValuesForConstructor File opts++-- | Gather filter pattern arguments into a list of account patterns and a+-- list of description patterns. We interpret pattern arguments as+-- follows: those prefixed with "desc:" are description patterns, all+-- others are account patterns; also patterns prefixed with "not:" are+-- negated. not: should come after desc: if both are used.+parsePatternArgs :: [String] -> ([String],[String])+parsePatternArgs args = (as, ds')+    where+      descprefix = "desc:"+      (ds, as) = partition (descprefix `isPrefixOf`) args+      ds' = map (drop (length descprefix)) ds++-- | Convert application options to the library's generic filter specification.+optsToFilterSpec :: [Opt] -> [String] -> LocalTime -> FilterSpec+optsToFilterSpec opts args t = FilterSpec {+                                datespan=dateSpanFromOpts (localDay t) opts+                               ,cleared=clearedValueFromOpts opts+                               ,real=Real `elem` opts+                               ,empty=Empty `elem` opts+                               ,costbasis=CostBasis `elem` opts+                               ,acctpats=apats+                               ,descpats=dpats+                               ,whichdate = if Effective `elem` opts then EffectiveDate else ActualDate+                               ,depth = depthFromOpts opts+                               }+    where (apats,dpats) = parsePatternArgs args++-- currentLocalTimeFromOpts opts = listtomaybe $ optValuesForConstructor CurrentLocalTime opts+--     where+--       listtomaybe [] = Nothing+--       listtomaybe vs = Just $ last vs+
+ Hledger/Cli/Tests.hs view
@@ -0,0 +1,1082 @@+{- |++This module contains hledger's unit tests. These are built in to hledger,+and can be run at any time by doing @hledger test@ (or, with a few more+options, by doing @make unittest@ in the hledger source tree.)++Other kinds of tests:++hledger's functional tests are a set of shell/command-line tests defined+by .test files in the tests\/ subdirectory. These can be run by doing+@make functest@ in the hledger source tree.++hledger's doctests are shell tests defined in literal blocks in haddock+documentation in the source, run by doing @make doctest@ in the hledger+source tree. They are no longer used, but here is an example:++@+$ hledger -f sample.ledger balance o+                  $1  expenses:food+                 $-2  income+                 $-1    gifts+                 $-1    salary+--------------------+                 $-1+@++-}++module Hledger.Cli.Tests+where+import qualified Data.Map as Map+import Test.HUnit.Tools (runVerboseTests)+import System.Exit (exitFailure, exitWith, ExitCode(ExitSuccess)) -- base 3 compatible+import System.Time (ClockTime(TOD))++import Hledger.Cli.Commands.All+import Hledger.Data  -- including testing utils in Hledger.Data.Utils+import Hledger.Cli.Options+import Hledger.Cli.Utils+++-- | Run unit tests.+runtests :: [Opt] -> [String] -> IO ()+runtests opts args = do+  (counts,_) <- runner ts+  if errors counts > 0 || (failures counts > 0)+   then exitFailure+   else exitWith ExitSuccess+    where+      runner | Verbose `elem` opts = runVerboseTests+             | otherwise = liftM (flip (,) 0) . runTestTT+      ts = TestList $ filter matchname $ tflatten tests  -- show flat test names+      -- ts = tfilter matchname $ TestList tests -- show hierarchical test names+      matchname = matchpats args . tname++-- | unit tests, augmenting the ones defined in each module. Where that is+-- inconvenient due to import cycles or whatever, we define them here.+tests :: Test+tests = TestList [+   tests_Hledger_Data,+   tests_Hledger_Commands,++   "account directive" ~:+   let sameParse str1 str2 = do j1 <- journalFromString str1+                                j2 <- journalFromString str2+                                j1 `is` j2{filereadtime=filereadtime j1, jtext=jtext j1}+   in TestList+   [+    "account directive 1" ~: sameParse +                          "2008/12/07 One\n  test:from  $-1\n  test:to  $1\n"+                          "!account test\n2008/12/07 One\n  from  $-1\n  to  $1\n"++   ,"account directive 2" ~: sameParse +                           "2008/12/07 One\n  test:foo:from  $-1\n  test:foo:to  $1\n"+                           "!account test\n!account foo\n2008/12/07 One\n  from  $-1\n  to  $1\n"++   ,"account directive 3" ~: sameParse +                           "2008/12/07 One\n  test:from  $-1\n  test:to  $1\n"+                           "!account test\n!account foo\n!end\n2008/12/07 One\n  from  $-1\n  to  $1\n"++   ,"account directive 4" ~: sameParse +                           ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" +++                            "!account outer\n2008/12/07 Two\n  aigh  $-2\n  bee  $2\n" +++                            "!account inner\n2008/12/07 Three\n  gamma  $-3\n  delta  $3\n" +++                            "!end\n2008/12/07 Four\n  why  $-4\n  zed  $4\n" +++                            "!end\n2008/12/07 Five\n  foo  $-5\n  bar  $5\n"+                           )+                           ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" +++                            "2008/12/07 Two\n  outer:aigh  $-2\n  outer:bee  $2\n" +++                            "2008/12/07 Three\n  outer:inner:gamma  $-3\n  outer:inner:delta  $3\n" +++                            "2008/12/07 Four\n  outer:why  $-4\n  outer:zed  $4\n" +++                            "2008/12/07 Five\n  foo  $-5\n  bar  $5\n"+                           )+   ]++  ,"accountnames" ~:+    accountnames ledger7 `is`+     ["assets","assets:cash","assets:checking","assets:saving","equity","equity:opening balances",+      "expenses","expenses:food","expenses:food:dining","expenses:phone","expenses:vacation",+      "liabilities","liabilities:credit cards","liabilities:credit cards:discover"]++  ,"accountNameTreeFrom" ~: do+    accountNameTreeFrom ["a"]       `is` Node "top" [Node "a" []]+    accountNameTreeFrom ["a","b"]   `is` Node "top" [Node "a" [], Node "b" []]+    accountNameTreeFrom ["a","a:b"] `is` Node "top" [Node "a" [Node "a:b" []]]+    accountNameTreeFrom ["a:b:c"]   `is` Node "top" [Node "a" [Node "a:b" [Node "a:b:c" []]]]++  ,"balance report tests" ~:+   let (opts,args) `gives` es = do +        l <- sampleledgerwithopts opts args+        t <- getCurrentLocalTime+        showBalanceReport opts (optsToFilterSpec opts args t) l `is` unlines es+   in TestList+   [++    "balance report with no args" ~:+    ([], []) `gives`+    ["                 $-1  assets"+    ,"                  $1    bank:saving"+    ,"                 $-2    cash"+    ,"                  $2  expenses"+    ,"                  $1    food"+    ,"                  $1    supplies"+    ,"                 $-2  income"+    ,"                 $-1    gifts"+    ,"                 $-1    salary"+    ,"                  $1  liabilities:debts"+    ]++   ,"balance report can be limited with --depth" ~:+    ([Depth "1"], []) `gives`+    ["                 $-1  assets"+    ,"                  $2  expenses"+    ,"                 $-2  income"+    ,"                  $1  liabilities"+    ]+    +   ,"balance report with account pattern o" ~:+    ([SubTotal], ["o"]) `gives`+    ["                  $1  expenses:food"+    ,"                 $-2  income"+    ,"                 $-1    gifts"+    ,"                 $-1    salary"+    ,"--------------------"+    ,"                 $-1"+    ]++   ,"balance report with account pattern o and --depth 1" ~:+    ([Depth "1"], ["o"]) `gives`+    ["                  $1  expenses"+    ,"                 $-2  income"+    ,"--------------------"+    ,"                 $-1"+    ]++   ,"balance report with account pattern a" ~:+    ([], ["a"]) `gives`+    ["                 $-1  assets"+    ,"                  $1    bank:saving"+    ,"                 $-2    cash"+    ,"                 $-1  income:salary"+    ,"                  $1  liabilities:debts"+    ,"--------------------"+    ,"                 $-1"+    ]++   ,"balance report with account pattern e" ~:+    ([], ["e"]) `gives`+    ["                 $-1  assets"+    ,"                  $1    bank:saving"+    ,"                 $-2    cash"+    ,"                  $2  expenses"+    ,"                  $1    food"+    ,"                  $1    supplies"+    ,"                 $-2  income"+    ,"                 $-1    gifts"+    ,"                 $-1    salary"+    ,"                  $1  liabilities:debts"+    ]++   ,"balance report with unmatched parent of two matched subaccounts" ~: +    ([], ["cash","saving"]) `gives`+    ["                 $-1  assets"+    ,"                  $1    bank:saving"+    ,"                 $-2    cash"+    ,"--------------------"+    ,"                 $-1"+    ]++   ,"balance report with multi-part account name" ~: +    ([], ["expenses:food"]) `gives`+    ["                  $1  expenses:food"+    ,"--------------------"+    ,"                  $1"+    ]++   ,"balance report with negative account pattern" ~:+    ([], ["not:assets"]) `gives`+    ["                  $2  expenses"+    ,"                  $1    food"+    ,"                  $1    supplies"+    ,"                 $-2  income"+    ,"                 $-1    gifts"+    ,"                 $-1    salary"+    ,"                  $1  liabilities:debts"+    ,"--------------------"+    ,"                  $1"+    ]++   ,"balance report negative account pattern always matches full name" ~: +    ([], ["not:e"]) `gives` []++   ,"balance report negative patterns affect totals" ~: +    ([], ["expenses","not:food"]) `gives`+    ["                  $1  expenses:supplies"+    ,"--------------------"+    ,"                  $1"+    ]++   ,"balance report with -E shows zero-balance accounts" ~:+    ([SubTotal,Empty], ["assets"]) `gives`+    ["                 $-1  assets"+    ,"                  $1    bank"+    ,"                  $0      checking"+    ,"                  $1      saving"+    ,"                 $-2    cash"+    ,"--------------------"+    ,"                 $-1"+    ]++   ,"balance report with cost basis" ~: do+      j <- journalFromString $ unlines+             [""+             ,"2008/1/1 test           "+             ,"  a:b          10h @ $50"+             ,"  c:d                   "+             ,""+             ]+      let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment+      showBalanceReport [] nullfilterspec j' `is`+       unlines+        ["                $500  a:b"+        ,"               $-500  c:d"+        ]++   ,"balance report elides zero-balance root account(s)" ~: do+      l <- journalFromStringWithOpts []+             (unlines+              ["2008/1/1 one"+              ,"  test:a  1"+              ,"  test:b"+              ])+      showBalanceReport [] nullfilterspec l `is`+       unlines+        ["                   1  test:a"+        ,"                  -1  test:b"+        ]++   ]++  ,"balanceTransaction" ~: do+     assertBool "detect unbalanced entry, sign error"+                    (isLeft $ balanceTransaction+                           (Transaction (parsedate "2007/01/28") Nothing False "" "test" ""+                            [Posting False "a" (Mixed [dollars 1]) "" RegularPosting Nothing, +                             Posting False "b" (Mixed [dollars 1]) "" RegularPosting Nothing+                            ] ""))+     assertBool "detect unbalanced entry, multiple missing amounts"+                    (isLeft $ balanceTransaction+                           (Transaction (parsedate "2007/01/28") Nothing False "" "test" ""+                            [Posting False "a" missingamt "" RegularPosting Nothing, +                             Posting False "b" missingamt "" RegularPosting Nothing+                            ] ""))+     let e = balanceTransaction (Transaction (parsedate "2007/01/28") Nothing False "" "test" ""+                           [Posting False "a" (Mixed [dollars 1]) "" RegularPosting Nothing, +                            Posting False "b" missingamt "" RegularPosting Nothing+                           ] "")+     assertBool "one missing amount should be ok" (isRight e)+     assertEqual "balancing amount is added" +                     (Mixed [dollars (-1)])+                     (case e of+                        Right e' -> (pamount $ last $ tpostings e')+                        Left _ -> error "should not happen")++  ,"journalCanonicaliseAmounts" ~:+   "use the greatest precision" ~:+    (map precision $ journalAmountAndPriceCommodities $ journalCanonicaliseAmounts $ journalWithAmounts ["1","2.00"]) `is` [2,2]++  ,"commodities" ~:+    Map.elems (commodities ledger7) `is` [Commodity {symbol="$", side=L, spaced=False, comma=False, precision=2}]++  ,"dateSpanFromOpts" ~: do+    let todaysdate = parsedate "2008/11/26"+    let gives = is . show . dateSpanFromOpts todaysdate+    [] `gives` "DateSpan Nothing Nothing"+    [Begin "2008", End "2009"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)"+    [Period "in 2008"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)"+    [Begin "2005", End "2007",Period "in 2008"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)"++  -- don't know what this should do+  -- ,"elideAccountName" ~: do+  --    (elideAccountName 50 "aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa"+  --     `is` "aa:aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa")+  --    (elideAccountName 20 "aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa"+  --     `is` "aa:aa:aaaaaaaaaaaaaa")++  ,"expandAccountNames" ~:+    expandAccountNames ["assets:cash","assets:checking","expenses:vacation"] `is`+     ["assets","assets:cash","assets:checking","expenses","expenses:vacation"]++  ,"intervalFromOpts" ~: do+    let gives = is . intervalFromOpts+    [] `gives` NoInterval+    [WeeklyOpt] `gives` Weekly+    [MonthlyOpt] `gives` Monthly+    [QuarterlyOpt] `gives` Quarterly+    [YearlyOpt] `gives` Yearly+    [Period "weekly"] `gives` Weekly+    [Period "monthly"] `gives` Monthly+    [Period "quarterly"] `gives` Quarterly+    [WeeklyOpt, Period "yearly"] `gives` Yearly++  ,"isAccountNamePrefixOf" ~: do+    "assets" `isAccountNamePrefixOf` "assets" `is` False+    "assets" `isAccountNamePrefixOf` "assets:bank" `is` True+    "assets" `isAccountNamePrefixOf` "assets:bank:checking" `is` True+    "my assets" `isAccountNamePrefixOf` "assets:bank" `is` False++  ,"isTransactionBalanced" ~: do+     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""+             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)+             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)+             ] ""+     assertBool "detect balanced" (isTransactionBalanced t)+     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""+             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)+             ,Posting False "c" (Mixed [dollars (-1.01)]) "" RegularPosting (Just t)+             ] ""+     assertBool "detect unbalanced" (not $ isTransactionBalanced t)+     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""+             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)+             ] ""+     assertBool "detect unbalanced, one posting" (not $ isTransactionBalanced t)+     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""+             [Posting False "b" (Mixed [dollars 0]) "" RegularPosting (Just t)+             ] ""+     assertBool "one zero posting is considered balanced for now" (isTransactionBalanced t)+     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""+             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)+             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)+             ,Posting False "d" (Mixed [dollars 100]) "" VirtualPosting (Just t)+             ] ""+     assertBool "virtual postings don't need to balance" (isTransactionBalanced t)+     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""+             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)+             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)+             ,Posting False "d" (Mixed [dollars 100]) "" BalancedVirtualPosting (Just t)+             ] ""+     assertBool "balanced virtual postings need to balance among themselves" (not $ isTransactionBalanced t)+     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""+             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)+             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)+             ,Posting False "d" (Mixed [dollars 100]) "" BalancedVirtualPosting (Just t)+             ,Posting False "e" (Mixed [dollars (-100)]) "" BalancedVirtualPosting (Just t)+             ] ""+     assertBool "balanced virtual postings need to balance among themselves (2)" (isTransactionBalanced t)++  ,"isSubAccountNameOf" ~: do+    "assets" `isSubAccountNameOf` "assets" `is` False+    "assets:bank" `isSubAccountNameOf` "assets" `is` True+    "assets:bank:checking" `isSubAccountNameOf` "assets" `is` False+    "assets:bank" `isSubAccountNameOf` "my assets" `is` False++  ,"default year" ~: do+    rl <- journalFromString defaultyear_ledger_str+    tdate (head $ jtxns rl) `is` fromGregorian 2009 1 1+    return ()++  ,"ledgerFile" ~: do+    assertBool "ledgerFile should parse an empty file" (isRight $ parseWithCtx emptyCtx ledgerFile "")+    r <- journalFromString "" -- don't know how to get it from ledgerFile+    assertBool "ledgerFile parsing an empty file should give an empty ledger" $ null $ jtxns r++  ,"normaliseMixedAmount" ~: do+     normaliseMixedAmount (Mixed []) ~?= Mixed [nullamt]++  ,"parsedate" ~: do+    parsedate "2008/02/03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1+    parsedate "2008-02-03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1++  ,"period expressions" ~: do+    let todaysdate = parsedate "2008/11/26"+    let str `gives` result = show (parsewith (periodexpr todaysdate) str) `is` ("Right " ++ result)+    "from aug to oct"           `gives` "(NoInterval,DateSpan (Just 2008-08-01) (Just 2008-10-01))"+    "aug to oct"                `gives` "(NoInterval,DateSpan (Just 2008-08-01) (Just 2008-10-01))"+    "every day from aug to oct" `gives` "(Daily,DateSpan (Just 2008-08-01) (Just 2008-10-01))"+    "daily from aug"            `gives` "(Daily,DateSpan (Just 2008-08-01) Nothing)"+    "every week to 2009"        `gives` "(Weekly,DateSpan Nothing (Just 2009-01-01))"++  ,"print report tests" ~: TestList+  [++   "print expenses" ~:+   do +    let args = ["expenses"]+    l <- sampleledgerwithopts [] args+    t <- getCurrentLocalTime+    showTransactions (optsToFilterSpec [] args t) l `is` unlines+     ["2008/06/03 * eat & shop"+     ,"    expenses:food                $1"+     ,"    expenses:supplies            $1"+     ,"    assets:cash                 $-2"+     ,""+     ]++  , "print report with depth arg" ~:+   do +    l <- sampleledger+    t <- getCurrentLocalTime+    showTransactions (optsToFilterSpec [Depth "2"] [] t) l `is` unlines+      ["2008/01/01 income"+      ,"    income:salary           $-1"+      ,""+      ,"2008/06/01 gift"+      ,"    income:gifts           $-1"+      ,""+      ,"2008/06/03 * eat & shop"+      ,"    expenses:food                $1"+      ,"    expenses:supplies            $1"+      ,"    assets:cash                 $-2"+      ,""+      ,"2008/12/31 * pay off"+      ,"    liabilities:debts            $1"+      ,""+      ]++  ]++  ,"punctuatethousands 1" ~: punctuatethousands "" `is` ""++  ,"punctuatethousands 2" ~: punctuatethousands "1234567.8901" `is` "1,234,567.8901"++  ,"punctuatethousands 3" ~: punctuatethousands "-100" `is` "-100"++  ,"register report tests" ~:+  let registerdates = filter (not . null) .  map (strip . take 10) . lines+  in+  TestList+  [++   "register report with no args" ~:+   do +    l <- sampleledger+    showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines+     ["2008/01/01 income               assets:bank:checking             $1           $1"+     ,"                                income:salary                   $-1            0"+     ,"2008/06/01 gift                 assets:bank:checking             $1           $1"+     ,"                                income:gifts                    $-1            0"+     ,"2008/06/02 save                 assets:bank:saving               $1           $1"+     ,"                                assets:bank:checking            $-1            0"+     ,"2008/06/03 eat & shop           expenses:food                    $1           $1"+     ,"                                expenses:supplies                $1           $2"+     ,"                                assets:cash                     $-2            0"+     ,"2008/12/31 pay off              liabilities:debts                $1           $1"+     ,"                                assets:bank:checking            $-1            0"+     ]++  ,"register report with cleared option" ~:+   do +    let opts = [Cleared]+    l <- journalFromStringWithOpts opts sample_ledger_str+    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines+     ["2008/06/03 eat & shop           expenses:food                    $1           $1"+     ,"                                expenses:supplies                $1           $2"+     ,"                                assets:cash                     $-2            0"+     ,"2008/12/31 pay off              liabilities:debts                $1           $1"+     ,"                                assets:bank:checking            $-1            0"+     ]++  ,"register report with uncleared option" ~:+   do +    let opts = [UnCleared]+    l <- journalFromStringWithOpts opts sample_ledger_str+    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines+     ["2008/01/01 income               assets:bank:checking             $1           $1"+     ,"                                income:salary                   $-1            0"+     ,"2008/06/01 gift                 assets:bank:checking             $1           $1"+     ,"                                income:gifts                    $-1            0"+     ,"2008/06/02 save                 assets:bank:saving               $1           $1"+     ,"                                assets:bank:checking            $-1            0"+     ]++  ,"register report sorts by date" ~:+   do +    l <- journalFromStringWithOpts [] $ unlines+        ["2008/02/02 a"+        ,"  b  1"+        ,"  c"+        ,""+        ,"2008/01/01 d"+        ,"  e  1"+        ,"  f"+        ]+    registerdates (showRegisterReport [] (optsToFilterSpec [] [] t1) l) `is` ["2008/01/01","2008/02/02"]++  ,"register report with account pattern" ~:+   do+    l <- sampleledger+    showRegisterReport [] (optsToFilterSpec [] ["cash"] t1) l `is` unlines+     ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"+     ]++  ,"register report with account pattern, case insensitive" ~:+   do +    l <- sampleledger+    showRegisterReport [] (optsToFilterSpec [] ["cAsH"] t1) l `is` unlines+     ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"+     ]++  ,"register report with display expression" ~:+   do +    l <- sampleledger+    let gives displayexpr = +            (registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l) `is`)+                where opts = [Display displayexpr]+    "d<[2008/6/2]"  `gives` ["2008/01/01","2008/06/01"]+    "d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"]+    "d=[2008/6/2]"  `gives` ["2008/06/02"]+    "d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"]+    "d>[2008/6/2]"  `gives` ["2008/06/03","2008/12/31"]++  ,"register report with period expression" ~:+   do +    l <- sampleledger    +    let periodexpr `gives` dates = do+          l' <- sampleledgerwithopts opts []+          registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l') `is` dates+              where opts = [Period periodexpr]+    ""     `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]+    "2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]+    "2007" `gives` []+    "june" `gives` ["2008/06/01","2008/06/02","2008/06/03"]+    "monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"]+    "quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"]+    let opts = [Period "yearly"]+    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines+     ["2008/01/01 - 2008/12/31         assets:bank:saving               $1           $1"+     ,"                                assets:cash                     $-2          $-1"+     ,"                                expenses:food                    $1            0"+     ,"                                expenses:supplies                $1           $1"+     ,"                                income:gifts                    $-1            0"+     ,"                                income:salary                   $-1          $-1"+     ,"                                liabilities:debts                $1            0"+     ]+    let opts = [Period "quarterly"]+    registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l) `is` ["2008/01/01","2008/04/01","2008/10/01"]+    let opts = [Period "quarterly",Empty]+    registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]++  ]++  , "register report with depth arg" ~:+   do +    l <- sampleledger+    let opts = [Depth "2"]+    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines+     ["2008/01/01 income               income:salary                   $-1          $-1"+     ,"2008/06/01 gift                 income:gifts                    $-1          $-2"+     ,"2008/06/03 eat & shop           expenses:food                    $1          $-1"+     ,"                                expenses:supplies                $1            0"+     ,"                                assets:cash                     $-2          $-2"+     ,"2008/12/31 pay off              liabilities:debts                $1          $-1"+     ]++  ,"show dollars" ~: show (dollars 1) ~?= "$1.00"++  ,"show hours" ~: show (hours 1) ~?= "1.0h"++  ,"unicode in balance layout" ~: do+    l <- journalFromStringWithOpts []+      "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n"+    showBalanceReport [] (optsToFilterSpec [] [] t1) l `is` unlines+      ["                -100  актив:наличные"+      ,"                 100  расходы:покупки"]++  ,"unicode in register layout" ~: do+    l <- journalFromStringWithOpts []+      "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n"+    showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines+      ["2009/01/01 медвежья шкура       расходы:покупки                 100          100"+      ,"                                актив:наличные                 -100            0"]++  ,"fixSmartDateStr" ~: do+    let gives = is . fixSmartDateStr (parsedate "2008/11/26")+    "1999-12-02"   `gives` "1999/12/02"+    "1999.12.02"   `gives` "1999/12/02"+    "1999/3/2"     `gives` "1999/03/02"+    "19990302"     `gives` "1999/03/02"+    "2008/2"       `gives` "2008/02/01"+    "0020/2"       `gives` "0020/02/01"+    "1000"         `gives` "1000/01/01"+    "4/2"          `gives` "2008/04/02"+    "2"            `gives` "2008/11/02"+    "January"      `gives` "2008/01/01"+    "feb"          `gives` "2008/02/01"+    "today"        `gives` "2008/11/26"+    "yesterday"    `gives` "2008/11/25"+    "tomorrow"     `gives` "2008/11/27"+    "this day"     `gives` "2008/11/26"+    "last day"     `gives` "2008/11/25"+    "next day"     `gives` "2008/11/27"+    "this week"    `gives` "2008/11/24" -- last monday+    "last week"    `gives` "2008/11/17" -- previous monday+    "next week"    `gives` "2008/12/01" -- next monday+    "this month"   `gives` "2008/11/01"+    "last month"   `gives` "2008/10/01"+    "next month"   `gives` "2008/12/01"+    "this quarter" `gives` "2008/10/01"+    "last quarter" `gives` "2008/07/01"+    "next quarter" `gives` "2009/01/01"+    "this year"    `gives` "2008/01/01"+    "last year"    `gives` "2007/01/01"+    "next year"    `gives` "2009/01/01"+--     "last wed"     `gives` "2008/11/19"+--     "next friday"  `gives` "2008/11/28"+--     "next january" `gives` "2009/01/01"++  ,"subAccounts" ~: do+    l <- liftM (journalToLedger nullfilterspec) sampleledger+    let a = ledgerAccount l "assets"+    map aname (ledgerSubAccounts l a) `is` ["assets:bank","assets:cash"]++  -- ,"summarisePostingsInDateSpan" ~: do+  --   let gives (b,e,depth,showempty,ps) =+  --           (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`)+  --   let ps =+  --           [+  --            nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}+  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [dollars 2]}+  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food",          lpamount=Mixed [dollars 4]}+  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [dollars 8]}+  --           ]+  --   ("2008/01/01","2009/01/01",0,9999,False,[]) `gives` +  --    []+  --   ("2008/01/01","2009/01/01",0,9999,True,[]) `gives` +  --    [+  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31"}+  --    ]+  --   ("2008/01/01","2009/01/01",0,9999,False,ts) `gives` +  --    [+  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",          lpamount=Mixed [dollars 4]}+  --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining",   lpamount=Mixed [dollars 10]}+  --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}+  --    ]+  --   ("2008/01/01","2009/01/01",0,2,False,ts) `gives` +  --    [+  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [dollars 15]}+  --    ]+  --   ("2008/01/01","2009/01/01",0,1,False,ts) `gives` +  --    [+  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [dollars 15]}+  --    ]+  --   ("2008/01/01","2009/01/01",0,0,False,ts) `gives` +  --    [+  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [dollars 15]}+  --    ]++  ]++  +-- fixtures/test data++date1 = parsedate "2008/11/26"+t1 = LocalTime date1 midday++sampleledger = journalFromStringWithOpts [] sample_ledger_str+sampleledgerwithopts opts _ = journalFromStringWithOpts opts sample_ledger_str++sample_ledger_str = unlines+ ["; A sample ledger file."+ ,";"+ ,"; Sets up this account tree:"+ ,"; assets"+ ,";   bank"+ ,";     checking"+ ,";     saving"+ ,";   cash"+ ,"; expenses"+ ,";   food"+ ,";   supplies"+ ,"; income"+ ,";   gifts"+ ,";   salary"+ ,"; liabilities"+ ,";   debts"+ ,""+ ,"2008/01/01 income"+ ,"    assets:bank:checking  $1"+ ,"    income:salary"+ ,""+ ,"2008/06/01 gift"+ ,"    assets:bank:checking  $1"+ ,"    income:gifts"+ ,""+ ,"2008/06/02 save"+ ,"    assets:bank:saving  $1"+ ,"    assets:bank:checking"+ ,""+ ,"2008/06/03 * eat & shop"+ ,"    expenses:food      $1"+ ,"    expenses:supplies  $1"+ ,"    assets:cash"+ ,""+ ,"2008/12/31 * pay off"+ ,"    liabilities:debts  $1"+ ,"    assets:bank:checking"+ ,""+ ,""+ ,";final comment"+ ]++defaultyear_ledger_str = unlines+ ["Y2009"+ ,""+ ,"01/01 A"+ ,"    a  $1"+ ,"    b"+ ]++write_sample_ledger = writeFile "sample.ledger" sample_ledger_str++entry2_str = unlines+ ["2007/01/27 * joes diner"+ ,"    expenses:food:dining                      $10.00"+ ,"    expenses:gifts                            $10.00"+ ,"    assets:checking                          $-20.00"+ ,""+ ]++entry3_str = unlines+ ["2007/01/01 * opening balance"+ ,"    assets:cash                                $4.82"+ ,"    equity:opening balances"+ ,""+ ,"2007/01/01 * opening balance"+ ,"    assets:cash                                $4.82"+ ,"    equity:opening balances"+ ,""+ ,"2007/01/28 coopportunity"+ ,"  expenses:food:groceries                 $47.18"+ ,"  assets:checking"+ ,""+ ]++periodic_entry1_str = unlines+ ["~ monthly from 2007/2/2"+ ,"  assets:saving            $200.00"+ ,"  assets:checking"+ ,""+ ]++periodic_entry2_str = unlines+ ["~ monthly from 2007/2/2"+ ,"  assets:saving            $200.00         ;auto savings"+ ,"  assets:checking"+ ,""+ ]++periodic_entry3_str = unlines+ ["~ monthly from 2007/01/01"+ ,"    assets:cash                                $4.82"+ ,"    equity:opening balances"+ ,""+ ,"~ monthly from 2007/01/01"+ ,"    assets:cash                                $4.82"+ ,"    equity:opening balances"+ ,""+ ]++ledger1_str = unlines+ [""+ ,"2007/01/27 * joes diner"+ ,"  expenses:food:dining                    $10.00"+ ,"  expenses:gifts                          $10.00"+ ,"  assets:checking                        $-20.00"+ ,""+ ,""+ ,"2007/01/28 coopportunity"+ ,"  expenses:food:groceries                 $47.18"+ ,"  assets:checking                        $-47.18"+ ,""+ ,""+ ]++ledger2_str = unlines+ [";comment"+ ,"2007/01/27 * joes diner"+ ,"  expenses:food:dining                    $10.00"+ ,"  assets:checking                        $-47.18"+ ,""+ ]++ledger3_str = unlines+ ["2007/01/27 * joes diner"+ ,"  expenses:food:dining                    $10.00"+ ,";intra-entry comment"+ ,"  assets:checking                        $-47.18"+ ,""+ ]++ledger4_str = unlines+ ["!include \"somefile\""+ ,"2007/01/27 * joes diner"+ ,"  expenses:food:dining                    $10.00"+ ,"  assets:checking                        $-47.18"+ ,""+ ]++ledger5_str = ""++ledger6_str = unlines+ ["~ monthly from 2007/1/21"+ ,"    expenses:entertainment  $16.23        ;netflix"+ ,"    assets:checking"+ ,""+ ,"; 2007/01/01 * opening balance"+ ,";     assets:saving                            $200.04"+ ,";     equity:opening balances                         "+ ,""+ ]++ledger7_str = unlines+ ["2007/01/01 * opening balance"+ ,"    assets:cash                                $4.82"+ ,"    equity:opening balances                         "+ ,""+ ,"2007/01/01 * opening balance"+ ,"    income:interest                                $-4.82"+ ,"    equity:opening balances                         "+ ,""+ ,"2007/01/02 * ayres suites"+ ,"    expenses:vacation                        $179.92"+ ,"    assets:checking                                 "+ ,""+ ,"2007/01/02 * auto transfer to savings"+ ,"    assets:saving                            $200.00"+ ,"    assets:checking                                 "+ ,""+ ,"2007/01/03 * poquito mas"+ ,"    expenses:food:dining                       $4.82"+ ,"    assets:cash                                     "+ ,""+ ,"2007/01/03 * verizon"+ ,"    expenses:phone                            $95.11"+ ,"    assets:checking                                 "+ ,""+ ,"2007/01/03 * discover"+ ,"    liabilities:credit cards:discover         $80.00"+ ,"    assets:checking                                 "+ ,""+ ,"2007/01/04 * blue cross"+ ,"    expenses:health:insurance                 $90.00"+ ,"    assets:checking                                 "+ ,""+ ,"2007/01/05 * village market liquor"+ ,"    expenses:food:dining                       $6.48"+ ,"    assets:checking                                 "+ ,""+ ]++journal7 = Journal+          [] +          [] +          [+           txnTieKnot $ Transaction {+             tdate=parsedate "2007/01/01",+             teffectivedate=Nothing,+             tstatus=False,+             tcode="*",+             tdescription="opening balance",+             tcomment="",+             tpostings=[+              Posting {+                pstatus=False,+                paccount="assets:cash",+                pamount=(Mixed [dollars 4.82]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              },+              Posting {+                pstatus=False,+                paccount="equity:opening balances",+                pamount=(Mixed [dollars (-4.82)]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              }+             ],+             tpreceding_comment_lines=""+           }+          ,+           txnTieKnot $ Transaction {+             tdate=parsedate "2007/02/01",+             teffectivedate=Nothing,+             tstatus=False,+             tcode="*",+             tdescription="ayres suites",+             tcomment="",+             tpostings=[+              Posting {+                pstatus=False,+                paccount="expenses:vacation",+                pamount=(Mixed [dollars 179.92]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              },+              Posting {+                pstatus=False,+                paccount="assets:checking",+                pamount=(Mixed [dollars (-179.92)]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              }+             ],+             tpreceding_comment_lines=""+           }+          ,+           txnTieKnot $ Transaction {+             tdate=parsedate "2007/01/02",+             teffectivedate=Nothing,+             tstatus=False,+             tcode="*",+             tdescription="auto transfer to savings",+             tcomment="",+             tpostings=[+              Posting {+                pstatus=False,+                paccount="assets:saving",+                pamount=(Mixed [dollars 200]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              },+              Posting {+                pstatus=False,+                paccount="assets:checking",+                pamount=(Mixed [dollars (-200)]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              }+             ],+             tpreceding_comment_lines=""+           }+          ,+           txnTieKnot $ Transaction {+             tdate=parsedate "2007/01/03",+             teffectivedate=Nothing,+             tstatus=False,+             tcode="*",+             tdescription="poquito mas",+             tcomment="",+             tpostings=[+              Posting {+                pstatus=False,+                paccount="expenses:food:dining",+                pamount=(Mixed [dollars 4.82]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              },+              Posting {+                pstatus=False,+                paccount="assets:cash",+                pamount=(Mixed [dollars (-4.82)]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              }+             ],+             tpreceding_comment_lines=""+           }+          ,+           txnTieKnot $ Transaction {+             tdate=parsedate "2007/01/03",+             teffectivedate=Nothing,+             tstatus=False,+             tcode="*",+             tdescription="verizon",+             tcomment="",+             tpostings=[+              Posting {+                pstatus=False,+                paccount="expenses:phone",+                pamount=(Mixed [dollars 95.11]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              },+              Posting {+                pstatus=False,+                paccount="assets:checking",+                pamount=(Mixed [dollars (-95.11)]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              }+             ],+             tpreceding_comment_lines=""+           }+          ,+           txnTieKnot $ Transaction {+             tdate=parsedate "2007/01/03",+             teffectivedate=Nothing,+             tstatus=False,+             tcode="*",+             tdescription="discover",+             tcomment="",+             tpostings=[+              Posting {+                pstatus=False,+                paccount="liabilities:credit cards:discover",+                pamount=(Mixed [dollars 80]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              },+              Posting {+                pstatus=False,+                paccount="assets:checking",+                pamount=(Mixed [dollars (-80)]),+                pcomment="",+                ptype=RegularPosting,+                ptransaction=Nothing+              }+             ],+             tpreceding_comment_lines=""+           }+          ]+          []+          []+          ""+          ""+          (TOD 0 0)+          ""++ledger7 = journalToLedger nullfilterspec journal7++ledger8_str = unlines+ ["2008/1/1 test           "+ ,"  a:b          10h @ $40"+ ,"  c:d                   "+ ,""+ ]++timelogentry1_str  = "i 2007/03/11 16:19:00 hledger\n"+timelogentry1 = TimeLogEntry In (parsedatetime "2007/03/11 16:19:00") "hledger"++timelogentry2_str  = "o 2007/03/11 16:30:00\n"+timelogentry2 = TimeLogEntry Out (parsedatetime "2007/03/11 16:30:00") ""++a1 = Mixed [(hours 1){price=Just $ Mixed [Amount (comm "$") 10 Nothing]}]+a2 = Mixed [(hours 2){price=Just $ Mixed [Amount (comm "EUR") 10 Nothing]}]+a3 = Mixed $ amounts a1 ++ amounts a2++journalWithAmounts :: [String] -> Journal+journalWithAmounts as =+        Journal+        []+        []+        [t | a <- as, let t = nulltransaction{tdescription=a,tpostings=[nullposting{pamount=parse a,ptransaction=Just t}]}]+        []+        []+        ""+        ""+        (TOD 0 0)+        ""+    where parse = fromparse . parseWithCtx emptyCtx postingamount . (" "++)+
+ Hledger/Cli/Utils.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE CPP #-}+{-|++Utilities for top-level modules and ghci. See also Hledger.Data.IO and+Hledger.Data.Utils.++-}++module Hledger.Cli.Utils+    (+     withJournalDo,+     journalFromStringWithOpts,+     openBrowserOn+    )+where+import Control.Monad.Error+import Hledger.Data+import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec)+import System.Directory (doesFileExist)+import System.IO (stderr)+#if __GLASGOW_HASKELL__ <= 610+import System.IO.UTF8 (hPutStrLn)+#else+import System.IO (hPutStrLn)+#endif+import System.Exit+import System.Process (readProcessWithExitCode)+import System.Info (os)+++-- | Parse the user's specified journal file and run a hledger command on+-- it, or report a parse error. This function makes the whole thing go.+withJournalDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Journal -> IO ()) -> IO ()+withJournalDo opts args cmdname cmd = do+  -- We kludgily read the file before parsing to grab the full text, unless+  -- it's stdin, or it doesn't exist and we are adding. We read it strictly+  -- to let the add command work.+  f <- journalFilePathFromOpts opts+  fileexists <- doesFileExist f+  let creating = not fileexists && cmdname == "add"+      costify = (if CostBasis `elem` opts then journalConvertAmountsToCost else id)+      runcmd = cmd opts args . costify+  if creating+   then runcmd nulljournal+   else (runErrorT . parseJournalFile) f >>= either parseerror runcmd+    where parseerror e = hPutStrLn stderr e >> exitWith (ExitFailure 1)++-- | Get a journal from the given string and options, or throw an error.+journalFromStringWithOpts :: [Opt] -> String -> IO Journal+journalFromStringWithOpts opts s = do+    j <- journalFromString s+    let cost = CostBasis `elem` opts+    return $ (if cost then journalConvertAmountsToCost else id) j++-- | Attempt to open a web browser on the given url, all platforms.+openBrowserOn :: String -> IO ExitCode+openBrowserOn u = trybrowsers browsers u+    where+      trybrowsers (b:bs) u = do+        (e,_,_) <- readProcessWithExitCode b [u] ""+        case e of+          ExitSuccess -> return ExitSuccess+          ExitFailure _ -> trybrowsers bs u+      trybrowsers [] u = do+        putStrLn $ printf "Sorry, I could not start a browser (tried: %s)" $ intercalate ", " browsers+        putStrLn $ printf "Please open your browser and visit %s" u+        return $ ExitFailure 127+      browsers | os=="darwin"  = ["open"]+               | os=="mingw32" = ["start"]+               | otherwise     = ["sensible-browser","gnome-www-browser","firefox"]+    -- jeffz: write a ffi binding for it using the Win32 package as a basis+    -- start by adding System/Win32/Shell.hsc and follow the style of any+    -- other module in that directory for types, headers, error handling and+    -- what not.+    -- ::ShellExecute(NULL, "open", "www.somepage.com", NULL, NULL, SW_SHOWNORMAL);+    -- ::ShellExecute(NULL, "open", "firefox.exe", "www.somepage.com" NULL, SW_SHOWNORMAL);+
+ Hledger/Cli/Version.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE CPP #-}+{-+Version-related utilities. See the Makefile for details of our version+numbering policy.+-}++module Hledger.Cli.Version+where+import System.Info (os, arch)+import Hledger.Data.Utils++-- version and PATCHLEVEL are set by the makefile+version       = "0.10.0"++#ifdef PATCHLEVEL+patchlevel = "." ++ show PATCHLEVEL -- must be numeric !+#else+patchlevel = ""+#endif++progname      = "hledger"+timeprogname  = "hours"++buildversion  = version ++ patchlevel :: String++binaryfilename = prettify $ splitAtElement '.' buildversion :: String+                where+                  prettify (major:minor:bugfix:patches:[]) =+                      printf "hledger-%s.%s%s%s-%s-%s%s" major minor bugfix' patches' os' arch suffix+                          where+                            bugfix'+                                | bugfix `elem` ["0"{-,"98","99"-}] = ""+                                | otherwise = '.' : bugfix+                            patches'+                                | patches/="0" = '+' : patches+                                | otherwise = ""+                            (os',suffix)+                                | os == "darwin"  = ("mac","")+                                | os == "mingw32" = ("windows",".exe")+                                | otherwise       = (os,"")+                  prettify (major:minor:bugfix:[]) = prettify [major,minor,bugfix,"0"]+                  prettify (major:minor:[])        = prettify [major,minor,"0","0"]+                  prettify (major:[])              = prettify [major,"0","0","0"]+                  prettify []                      = error "VERSION is empty, please fix"+                  prettify _                       = error "VERSION has too many components, please fix"++versionstr    = prettify $ splitAtElement '.' buildversion :: String+                where+                  prettify (major:minor:bugfix:patches:[]) =+                      printf "%s.%s%s%s" major minor bugfix' patches'+                          where+                            bugfix'+                                | bugfix `elem` ["0"{-,"98","99"-}] = ""+                                | otherwise = '.' : bugfix+                            patches'+                                | patches/="0" = "+"++patches+                                | otherwise = ""+                  prettify s = intercalate "." s++versionmsg    = progname ++ "-" ++ versionstr ++ configmsg :: String+    where configmsg+              | null configflags = " with no extras"+              | otherwise = " with " ++ intercalate ", " configflags++configflags   = tail [""+#ifdef CHART+  ,"chart"+#endif+#ifdef VTY+  ,"vty"+#endif+#if defined(WEB)+  ,"web (using simpleserver)"+#elif defined(WEBHAPPSTACK)+  ,"web (using happstack)"+#endif+ ]
− HledgerMain.hs
@@ -1,47 +0,0 @@-{-# LANGUAGE CPP #-}-{-|-The main function is in this separate module so it can be imported by-benchmark scripts. As a side benefit, this avoids a weakness of sp, which-doesn't allow both #! and \{\-\# lines.--}--module HledgerMain where-#if __GLASGOW_HASKELL__ <= 610-import Prelude hiding (putStr, putStrLn)-import System.IO.UTF8-#endif--import Commands.All-import Ledger-import Options-import Tests-import Utils (withLedgerDo)-import Version (versionmsg, binaryfilename)--main :: IO ()-main = do-  (opts, cmd, args) <- parseArguments-  run cmd opts args-    where-      run cmd opts args-       | Help `elem` opts             = putStr usage-       | Version `elem` opts          = putStrLn versionmsg-       | BinaryFilename `elem` opts   = putStrLn binaryfilename-       | cmd `isPrefixOf` "balance"   = withLedgerDo opts args cmd balance-       | cmd `isPrefixOf` "convert"   = withLedgerDo opts args cmd convert-       | cmd `isPrefixOf` "print"     = withLedgerDo opts args cmd print'-       | cmd `isPrefixOf` "register"  = withLedgerDo opts args cmd register-       | cmd `isPrefixOf` "histogram" = withLedgerDo opts args cmd histogram-       | cmd `isPrefixOf` "add"       = withLedgerDo opts args cmd add-       | cmd `isPrefixOf` "stats"     = withLedgerDo opts args cmd stats-#ifdef VTY-       | cmd `isPrefixOf` "ui"        = withLedgerDo opts args cmd ui-#endif-#if defined(WEB) || defined(WEBHAPPSTACK)-       | cmd `isPrefixOf` "web"       = withLedgerDo opts args cmd web-#endif-#ifdef CHART-       | cmd `isPrefixOf` "chart"       = withLedgerDo opts args cmd chart-#endif-       | cmd `isPrefixOf` "test"      = runtests opts args >> return ()-       | otherwise                    = putStr usage
+ MANUAL.markdown view
@@ -0,0 +1,903 @@+---+title: hledger manual+---++# hledger manual++This is the official hledger manual. You may also want to visit the+[http://hledger.org](http://hledger.org) home page, the+[hledger for techies](README2.html) page, and for background,+[c++ ledger's manual](http://joyful.com/repos/ledger/doc/ledger.html).++## User Guide++### Introduction++hledger is a program for tracking money, time, or any other commodity,+using a plain text file format and the simple but powerful principles of+double-entry accounting.++It is modelled closely on+[John Wiegley's ledger](http://wiki.github.com/jwiegley/ledger) (aka "c+++ledger"), with some features removed and some new ones added. I wrote+hledger because I wanted to build financial tools in the Haskell+programming language rather than in C++.++hledger's basic function is to generate register and balance reports from+a plain text ledger file, at the command line or via the web or curses+interface. You can use it to, eg,++-   track spending and income+-   see time reports by day/week/month/project+-   get accurate numbers for client billing and tax filing+-   track invoices++hledger aims to help both computer experts and every-day users gain+clarity in their finances and time management. For now though, it is most+useful to technically-minded folks who are comfortable with command-line+tools.++hledger is copyright (c) 2007-2009 Simon Michael+<[simon@joyful.com](mailto:simon@joyful.com)\> and contributors and+released as Free Software under GPL version 3 or later.++### Installing++hledger works on all major platforms; here are the [release+notes](http://hledger.org/NEWS.html). One of these pre-built+[binaries](http://hledger.org/binaries/) might work for you, but at+present these are not very up-to-date, so the usual thing is to build+with the cabal-install tool:++1. If you don't already have the Glasgow Haskell Compiler and+   cabal-install, download and install the+   [Haskell Platform](http://hackage.haskell.org/platform/).  Or, you may+   be able to use platform packages; eg on Ubuntu Lucid, do `apt-get+   install ghc6 cabal-install happy`.++2. Make sure ~/.cabal/bin is in your path. This is useful so that you can+   run hledger by just typing "hledger", and necessary if (eg) you install+   with -fweb, to avoid an installation failure..++3. Install hledger with cabal-install:++        cabal update+        cabal install hledger++    You can add the following options to the install command to include extra features+    (these are not enabled by default as they are harder to build):++    - `-fvty` - builds the [ui](#ui) command. (Not available on microsoft+        windows.)++    - `-fweb` - builds the [web](#web) command.++    - `-fchart` builds the [chart](#chart) command. This requires+        [gtk2hs](http://www.haskell.org/gtk2hs/download/), which you'll+        need to install yourself as it's not yet provided by the haskell+        platform or cabal.++#### Installation Problems++The above builds a lot of software, and it's not always smooth sailing.+Here are some known issues and things to try:++- **Ask for help on [#hledger](irc://freenode.net/#hledger) or [#haskell](irc://freenode.net/#haskell)**++- **cabal update.** If you didn't already, ``cabal update`` and try again.++- **Do you have a new enough version of GHC ?** As of 2010, 6.10 and 6.12+    are supported, 6.8 might or might not work.++- **Do you have a new enough version of cabal-install ?**+  Newer versions tend to be better at resolving problems. 0.6.2 has been+  known to fail where newer versions succeed.++- **Installation fails, reporting problems with a hledger package.**+  That hledger release might have a coding error (heavens), or+  compatibility problems have been revealed as dependencies have been+  updated.  You could try installing the [previous hledger+  version](http://hackage.haskell.org/package/hledger) (``cabal+  install hledger-0.x``) or, preferably, the latest hledger+  development code, which is likely to work best.  In a nutshell,+  install [darcs](http://darcs.net) and:++        darcs get --lazy http://joyful.com/repos/hledger+        cd hledger/hledger-lib; cabal install+        cd ..; cabal install [-fweb] [-fvty]++- **Installation fails, reporting problems with packages A, B and C.**+  Resolve the problem packages one at a time. Eg, cabal install A.  Look+  for the cause of the failure near the end of the output. If it's not+  apparent, try again with `-v2` or `-v3` for more verbose output.++- **Could not run trhsx.**+  You are installing -fweb, which needs to run the ``trhsx`` executable.+  It is usually installed in ~/.cabal/bin, which may not be in your path.++- **Could not run happy.**+  A package (eg haskell-src-exts) needs to run the ``happy`` executable.+  If not using the haskell platform, install the appropriate platform+  package which provides it (eg apt-get install happy).++- **A ghc panic while building** might be due to+    [http://hackage.haskell.org/trac/ghc/ticket/3862](http://hackage.haskell.org/trac/ghc/ticket/3862)++- **cabal could not reconcile dependencies**+  In some cases, especially if you have installed/updated many packages or+  GHC itself, cabal may not be able to figure out the installation.  This+  can also arise due to non-optimal dependency information configured in+  hledger or its dependencies. You can sometimes work around this by using+  cabal's `--constraint` option.++### Basic usage++Basic usage is:++    hledger [OPTIONS] [COMMAND [PATTERNS]]++[OPTIONS](#overview) may appear anywhere on the command line.+[COMMAND](#commands) is one of: add, balance, chart, convert, histogram,+print, register, stats, ui, web, test (defaulting to balance). The+optional [PATTERNS](#filter-patterns) are regular expressions which select+a subset of the ledger data.++hledger looks for data in a ledger file, usually `.ledger` in your home+directory. You can specify a different file with the -f option (use - for+standard input) or `LEDGER` environment variable.++To get started, make yourself a ledger file containing some+transactions. You can copy the sample file below (or+[sample.ledger](http://joyful.com/repos/hledger/sample.ledger)) and save+it as `.ledger` in your home directory. Or, just run `hledger add` and+enter a few transactions. Now you can try some of these commands, or read+on:++    hledger --help                        # show command-line help+    hledger balance                       # all accounts with aggregated balances+    hledger bal --depth 1                 # only top-level accounts+    hledger register                      # transaction register+    hledger reg income                    # transactions to/from an income account+    hledger reg checking                  # checking transactions+    hledger reg desc:shop                 # transactions with shop in the description+    hledger histogram                     # transactions per day, or other interval+    hledger add                           # add some new transactions to the ledger file+    hledger ui                            # curses ui, if installed with -fvty+    hledger web                           # web ui, if installed with -fweb+    hledger chart                         # make a balance chart, if installed with -fchart++You'll find more examples below.++### File format++hledger's data file, aka the ledger, is a plain text representation of a+standard accounting journal. It contains a number of transactions, each+describing a transfer of money (or another commodity) between two or more+named accounts. Here's an example:++    ; A sample ledger file. This is a comment.+    +    2008/01/01 income               ; <- transaction's first line starts in column 0, contains date and description+        assets:bank:checking  $1    ; <- posting lines start with whitespace, each contains an account name+        income:salary        $-1    ;    followed by at least two spaces and an amount+    +    2008/06/01 gift+        assets:bank:checking  $1    ; <- at least two postings in a transaction+        income:gifts         $-1    ; <- their amounts must balance to 0+    +    2008/06/02 save+        assets:bank:saving    $1+        assets:bank:checking        ; <- one amount may be omitted; here $-1 is inferred+    +    2008/06/03 eat & shop           ; <- description can be anything+        expenses:food         $1+        expenses:supplies     $1    ; <- this transaction debits two expense accounts+        assets:cash                 ; <- $-2 inferred+    +    2008/12/31 * pay off            ; <- an optional * after the date means "cleared" (or anything you want)+        liabilities:debts     $1+        assets:bank:checking++Each transaction has a date, description, and two or more postings (of+some amount to some account) which must balance to 0. As a convenience,+one posting's amount may be left blank and will be inferred.++Note that account names may contain single spaces, while the amount must+be separated from the account name by at least two spaces.++An amount is a number, with an optional currency/commodity symbol or word+on either the left or right. Note: when writing a negative amount with a+left-side currency symbol, the minus goes after the symbol, eg `$-1`.++This file format is also compatible with c++ ledger, so you can use both+tools. For more details, see+[File format compatibility](#file-format-compatibility).++## Reference++### Overview++This version of hledger mimics a subset of ledger 3.x, and adds some+features of its own. We currently support regular ledger entries, timelog+entries, multiple commodities, (fixed) price history, virtual postings,+filtering by account and description, the familiar print, register &+balance commands and several new commands. We handle (almost) the full+period expression syntax, and very limited display expressions consisting+of a simple date predicate.++Here is the command-line help:++    Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]+           hledger [OPTIONS] convert CSVFILE+           hledger [OPTIONS] stats+    +    hledger uses your ~/.ledger or $LEDGER file, or another specified with -f+    +    COMMAND is one of (may be abbreviated):+     add       - prompt for new transactions and add them to the ledger+     balance   - show accounts, with balances+     convert   - read CSV bank data and display in ledger format+     histogram - show a barchart of transactions per day or other interval+     print     - show transactions in ledger format+     register  - show transactions as a register with running balance+     stats     - show various statistics for a ledger+     ui        - run a simple text-based UI+     web       - run a simple web-based UI+     chart     - generate balances pie chart+     test      - run self-tests+    +    PATTERNS are regular expressions which filter by account name.+    Prefix with desc: to filter by transaction description instead.+    Prefix with not: to negate a pattern. When using both, not: comes last.+    +    DATES can be y/m/d or ledger-style smart dates like "last month".+    +    Options:+     -f FILE  --file=FILE          use a different ledger/timelog file; - means stdin+              --no-new-accounts    don't allow to create new accounts+     -b DATE  --begin=DATE         report on transactions on or after this date+     -e DATE  --end=DATE           report on transactions before this date+     -p EXPR  --period=EXPR        report on transactions during the specified period+                                   and/or with the specified reporting interval+     -C       --cleared            report only on cleared transactions+     -U       --uncleared          report only on uncleared transactions+     -B       --cost, --basis      report cost of commodities+              --depth=N            hide accounts/transactions deeper than this+     -d EXPR  --display=EXPR       show only transactions matching EXPR (where+                                   EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)+              --effective          use transactions' effective dates, if any+     -E       --empty              show empty/zero things which are normally elided+     -R       --real               report only on real (non-virtual) transactions+              --no-total           balance report: hide the final total+     -W       --weekly             register report: show weekly summary+     -M       --monthly            register report: show monthly summary+     -Q       --quarterly          register report: show quarterly summary+     -Y       --yearly             register report: show yearly summary+     -h       --help               show this help+     -V       --version            show version information+     -v       --verbose            show verbose test output+              --binary-filename    show the download filename for this hledger build+              --debug              show extra debug output; implies verbose+              --debug-no-ui        run ui commands with no output+     -o FILE  --output=FILE        chart: output filename (default: hledger.png)+              --items=N            chart: number of accounts to show (default: 10)+              --size=WIDTHxHEIGHT  chart: image size (default: 600x400)++### Commands++#### Reporting commands++These commands are read-only, that is they never modify your data.++##### print++The print command displays full transactions from the ledger file, tidily+formatted and showing all amounts explicitly. The output of print is+always valid ledger data.++hledger's print command also shows all unit prices in effect, or (with+-B/--cost) shows cost amounts.++Examples:++    $ hledger print+    $ hledger print employees:bob | hledger -f- register expenses++##### register++The register command displays postings, one per line, and their running+total. With a [reporting interval](#reporting-interval) it will aggregate+similar postings within each interval.++Examples:++    $ hledger register+    $ hledger register --monthly -E rent++##### balance++The balance command displays accounts and their balances.++Examples:++    $ hledger balance+    $ hledger balance food -p 'last month'+    $ for y in 2006 2007 2008 2009 2010; do echo; echo $y; hledger -f $y.ledger balance ^expenses --depth 2; done++##### chart++(optional feature)++The chart command saves a pie chart of your top account balances to an+image file (usually "hledger.png", or use -o/--output FILE). You can+adjust the image resolution with --size=WIDTHxHEIGHT, and the number of+accounts with --items=N.++Note that positive and negative balances will not be displayed together in+the same chart; any balances not matching the sign of the first one will+be omitted.++To show only accounts above a certain depth, use the --depth+option. Otherwise, the chart can include accounts at any depth. If a+parent and child account are both displayed, the parent's balance excludes+the child's.++Examples:++    $ hledger chart assets --depth 2+    $ hledger chart liabilities --depth 2+    $ hledger chart ^expenses -o balance.png --size 1000x600 --items 20+    $ for m in 01 02 03 04 05 06 07 08 09 10 11 12; do hledger -p 2009/$m chart ^expenses --depth 2 -o expenses-2009$m.png --size 400x300; done++##### histogram++The histogram command displays a quick bar chart showing transaction+counts, per day, week, month or other reporting interval. It is+experimental.++Examples:++    $ hledger histogram -p weekly dining++##### stats++The stats command displays quick summary information for the ledger.++Examples:++    $ hledger stats++##### ui++(optional feature)++The ui command starts hledger's curses (full-screen, text) user interface,+which allows interactive navigation of the print/register/balance+reports. This lets you browse around your numbers and get quick insights+with less typing.++Examples:++$ hledger ui $ hledger ui -BE food++#### Modifying commands++The following commands can alter your ledger file.++##### add++The add command prompts interactively for new transactions, and adds them+to the ledger. It is experimental.++Examples:++$ hledger add $ hledger add accounts:personal:bob++##### web++(optional feature)++The web command starts hledger's web interface, and tries to open a web+browser to view it (if this fails, you'll have to visit the indicated url+yourself.) The web ui combines the features of the print, register,+balance and add commands.++Examples:++    $ hledger web+    $ hledger web --debug -f demo.ledger -p thisyear++#### Other commands++##### convert++The convert command reads a+[CSV](http://en.wikipedia.org/wiki/Comma-separated_values) file you have+downloaded from your bank, and prints out the transactions in ledger+format, suitable for adding to your ledger. It does not alter your ledger+directly.++This can be a lot quicker than entering every transaction by hand.  (The+downside is that you are less likely to notice if your bank makes an+error!) Use it like this:++    $ hledger convert FILE.csv >FILE.ledger++where FILE.csv is your downloaded csv file. This will convert the csv data+using conversion rules defined in FILE.rules (auto-creating this file if+needed), and save the output into a temporary ledger file. Then you should+review FILE.ledger for problems; update the rules and convert again if+needed; and finally copy/paste transactions which are new into your main+ledger.++###### .rules file++convert requires a \*.rules file containing data definitions and rules for+assigning destination accounts to transactions; it will be auto-created if+missing. Typically you will have one csv file and one rules file per bank+account. Here's an example rules file for converting csv data from a Wells+Fargo checking account:++    base-account assets:bank:checking+    date-field 0+    description-field 4+    amount-field 1+    currency $+    +    # account-assigning rules+    +    SPECTRUM+    expenses:health:gym+    +    ITUNES+    BLKBSTR=BLOCKBUSTER+    expenses:entertainment+    +    (TO|FROM) SAVINGS+    assets:bank:savings++This says:++-   the ledger account corresponding to this csv file is+    assets:bank:checking+-   the first csv field is the date, the second is the amount, the+    fifth is the description+-   prepend a dollar sign to the amount field+-   if description contains SPECTRUM (case-insensitive), the+    transaction is a gym expense+-   if description contains ITUNES or BLKBSTR, the transaction is+    an entertainment expense; also rewrite BLKBSTR as BLOCKBUSTER+-   if description contains TO SAVINGS or FROM SAVINGS, the+    transaction is a savings transfer++Notes:++-   Lines beginning with \# or ; are ignored (but avoid using+    inside an account rule)++-   Definitions must come first, one per line, all in one+    paragraph. Each is a name and a value separated by whitespace.+    Supported names are: base-account, date-field, status-field,+    code-field, description-field, amount-field, currency-field,+    currency. All are optional and will use defaults if not specified.++-   The remainder of the file is account-assigning rules. Each is a+    paragraph consisting of one or more description-matching patterns+    (case-insensitive regular expressions), one per line, followed by+    the account name to use when the transaction's description matches+    any of these patterns.++-   A match pattern may be followed by a replacement pattern,+    separated by `=`, which rewrites the matched part of the+    description. Use this if you want to clean up messy bank data. To+    rewrite the entire description, use a match pattern like+    `.*PAT.*=REPL`. Within a replacement pattern, you can refer to the+    matched text with `\0` and any regex groups with `\1`, `\2` in the+    usual way.+++##### test++This command runs hledger's internal self-tests and displays a quick+report. The -v option shows more detail, and a pattern can be provided to+filter tests by name. It's mainly used in development, but it's also nice+to be able to run a sanity check at any time..++Examples:++    $ hledger test+    $ hledger test -v balance++### Other features++#### Filter patterns++Most commands accept one or more filter pattern arguments after the+command name, to select a subset of transactions or postings. There are+two kinds of pattern:++-   an account pattern, which is a regular expression. This is+    matched against postings' accounts. Optionally, it may be prefixed+    with `not:` in which case the match is negated.++-   a description pattern, like the above but prefixed with+    `desc:`. This is matched against transactions' descriptions. Note,+    when negating a desc: pattern, not: goes last, eg:+    `desc:not:someregexp`.+++When you specify multiple filter patterns, hledger generally selects the+transactions or postings which match (or negatively match)++> *any of the account patterns* AND+> *any of the description patterns*++The [print](#print) command selects transactions which++> *match any of the description patterns* AND+> *have any postings matching any of the positive account patterns*+> AND+> *have no postings matching any of the negative account patterns*++#### Dates++##### Simple dates++Within a ledger file, dates must follow a fairly simple year/month/day+format. Examples:++> `2010/01/31` or `2010/1/31` or `2010-1-31` or `2010.1.31`++The [add](#add) command and the [web](#web) add form, as well as some+other places, accept [smart dates](#smart-dates) - more about those below.++##### Default year++You can set a default year with a `Y` directive in the ledger, then+subsequent dates may be written as month/day. Eg:++    Y2009+    +    12/15 ...+    +    Y2010+    +    1/31 ...++##### Actual and effective dates++Frequently, a real-life transaction has two (or more) dates of+interest. For example, you might make a purchase on friday with a debit+card, and it might clear (take effect in your bank account) on+tuesday. It's sometimes useful to model this accurately, so that your+hledger balances match reality. So, you can specify two dates for a+transaction, the *actual* and *effective* date, separated by `=`. Eg:++    2010/2/19=2010/2/23 ...++Then you can use the `--effective` flag to prefer the effective (second)+date, if any, in reports. This is useful for, eg, seeing a more accurate+daily balance while reconciling a bank account.++So, what do *actual* and *effective* mean, exactly ? I always assumed that+the actual date comes first, and is "the date you enacted the+transaction", while the effective date comes second, and is optional, and+is "the date the transaction took effect" (showed up in your bank+balance).++Unfortunately, this is the reverse of c++ ledger's interpretation (cf+[differences](#other-differences)). However it's not *too* serious; just+remember that hledger requires the first date; allows an optional second+date which the `--effective` flag will select; and the meaning of "actual"+and "effective" is up to you.++The second date may omit the year if it is the same as the first's.++##### Smart dates++In [period expressions](#period-expressions), the `-b` and `-e` options,+the [add](#add) command and the [web](#web) add form, more flexible "smart+dates" are allowed. Here are some examples:++-   `2009/1/1`, `2009/01/01`, `2009-1-1`, `2009.1.1`, `2009/1`,+    `2009` (january 1, 2009)+-   `1/1`, `january`, `jan`, `this year` (january 1, this year)+-   `next year` (january 1, next year)+-   `this month` (the 1st of the current month)+-   `this week` (the most recent monday)+-   `last week` (the monday of the week before this one)+-   `today`, `yesterday`, `tomorrow`++Spaces are optional, so eg: `-p lastmonth` is valid.++#### Period expressions++hledger supports flexible "period expressions" with the `-p/--period`+option to select transactions within a period of time (like 2009) and/or+with a reporting interval (like weekly). hledger period expressions are+similar but not identical to c++ ledger's.++Here is a basic period expression specifying the first quarter of 2009+(start date is always included, end date is always excluded):++    -p "from 2009/1/1 to 2009/4/1"++Keywords like "from" and "to" are optional, and so are the spaces.  Just+don't run two dates together:++    -p2009/1/1to2009/4/1+    -p"2009/1/1 2009/4/1"++Dates are [smart dates](#smart-dates), so if the current year is 2009, the+above can also be written as:++    -p "1/1 to 4/1"+    -p "january to apr"+    -p "this year to 4/1"++If you specify only one date, the missing start or end date will be the+earliest or latest transaction in your ledger data:++    -p "from 2009/1/1"  (everything after january 1, 2009)+    -p "from 2009/1"    (the same)+    -p "from 2009"      (the same)+    -p "to 2009"        (everything before january 1, 2009)++A single date with no "from" or "to" defines both the start and end date+like so:++    -p "2009"           (the year 2009;    equivalent to "2009/1/1 to 2010/1/1")+    -p "2009/1"         (the month of jan; equivalent to "2009/1/1 to 2009/2/1")+    -p "2009/1/1"       (just that day;    equivalent to "2009/1/1 to 2009/1/2")++##### Reporting interval++You can also specify a reporting interval, which causes the "register"+command to summarise the transactions in each interval.  It goes before+the dates, and can be: "daily", "weekly", "monthly", "quarterly", or+"yearly". An "in" keyword is optional, and so are the dates:++    -p "weekly from 2009/1/1 to 2009/4/1"+    -p "monthly in 2008"+    -p "monthly from 2008"+    -p "quarterly"++A reporting interval may also be specified with the -W/--weekly,+-M/--monthly, -Q/--quarterly, and -Y/--yearly options. However..++##### -p overrides other flags++Note: any period option on the command line will override the -b, -e, -W,+-Q and -Y flags.++#### Display expressions++A display expression with the `-d/--display` option selects which+transactions will be displayed (unlike a+[period expression](#period-expressions), which selects the transactions+to be used for calculation).++hledger currently supports a very small subset of c++ ledger's display+expressions, namely: transactions before or after a date.  This is useful+for displaying your recent check register with an accurate running+total. Note the use of \>= here to include the first of the month:++    $ hledger register -d "d>=[this month]"++#### Depth limiting++With the `--depth N` option, reports will show only the uppermost accounts+in the account tree, down to level N. This is most useful with+[balance](#balance) (and [chart](#chart)). For example:++    $ hledger balance --depth 2++shows a more concise balance report displaying only the top two levels of+accounts. This example with [register](#register):++    $ hledger register --depth 1++would show only the postings to top-level accounts, which usually means+none.++#### Prices++You can specify a commodity's unit price, or exchange rate, in terms of+another commodity. There are two ways.++First, you can set the price explicitly for a single posting by writing `@+PRICE` after the amount. PRICE is another amount in a different+commodity. Eg, here one hundred euros was purchased at $1.35 per euro:++    2009/1/2 x+     expenses:foreign currency       €100 @ $1.35+     assets++Secondly, you can set the price for a commodity as of a certain date, by+entering a historical price record. These are lines beginning with "P",+appearing anywhere in the ledger between transactions. Eg, here we say the+exchange rate for 1 euro is $1.35 on 2009/1/1 (and thereafter, until a+newer price record is found):++    P 2009/1/1 € $1.35  ; <- historical price: P, date, commodity symbol, price in 2nd commodity (space-separated)+    +    2009/1/2 x+     expenses:foreign currency       €100+     assets++The print command shows any unit prices in effect. Either example above+will show:++    $ hledger print+    2009/01/02 x+        expenses:foreign currency  €100 @ $1.35+        assets                     €-100 @ $1.35++To see amounts converted to their total cost, use the `--cost/-B` flag+with any command:++    $ hledger print --cost+    2009/01/02 x+        expenses:foreign currency       $135.00+        assets                         $-135.00++The `--cost/-B` flag does only one lookup step, ie it will not look up the+price of a price's commodity.++Note hledger handles prices differently from c++ ledger in one important+respect: we assume unit prices do not vary over time.  This is good for+simple reporting of foreign currency transactions, but not for tracking+fluctuating-value investments or capital gains.++#### Timelog reporting++hledger will also read timelog files in timeclock.el format. As a+convenience, if you invoke hledger via an "hours" symlink or copy, it uses+your timelog file (\~/.timelog or $TIMELOG) by default, rather than your+ledger.++Timelog entries look like this:++    i 2009/03/31 22:21:45 some:project+    o 2009/04/01 02:00:34++The clockin description is treated as an account name. Here are some+queries to try (after downloading+[sample.timelog](http://joyful.com/repos/hledger/sample.timelog)):++    ln -s `which hledger` ~/bin/hours            # set up "hours" in your path+    export TIMELOG=sample.timelog+    hours                                        # show all time balances+    hours -p 'last week'                         # last week+    hours -p thismonth                           # the space is optional+    hours -p 'from 1/15' register project        # project sessions since jan 15+    hours -p 'weekly' reg --depth 1 -E           # weekly time summary++This is a useful feature, if you can find a way to efficiently record+timelog entries. The "ti" and "to" scripts may be available from the c+++ledger 2.x repository. I use+[timeclock-x.el](http://www.emacswiki.org/emacs/timeclock-x.el) and+[ledgerutils.el](http://joyful.com/repos/ledgertools/ledgerutils.el) in+emacs.++### Compatibility with c++ ledger++#### Implementation++Unlike c++ ledger, hledger is written in the Haskell programming+language. Haskell enables a coding style known as pure lazy functional+programming, which holds the promise of more robust and maintainable+software built with fewer lines of code. Haskell also provides a more+abstracted, portable platform which can make deployment and installation+easier in some cases. Haskell also brings some new challenges such as+managing memory growth.++#### File format compatibility++hledger's file format is mostly identical with that of c++ ledger version+2, with some features (like modifier and periodic entries) being accepted,+but ignored. There are also some subtle differences in parser behaviour+(eg comments may be permissible in different places.) C++ ledger version 3+has introduced additional syntax, which current hledger probably fails to+parse.++Generally, it's easy to keep a ledger file that works with both hledger+and c++ledger if you avoid the more esoteric syntax.  Occasionally you'll+need to make small edits to restore compatibility for one or the other.++#### Features not supported++c++ ledger features not currently supported include: modifier and periodic+entries, and the following c++ ledger options and commands:++    Basic options:+    -o, --output FILE      write output to FILE+    -i, --init-file FILE   initialize ledger using FILE (default: ~/.ledgerrc)+    -a, --account NAME     use NAME for the default account (useful with QIF)+    +    Report filtering:+    -c, --current          show only current and past entries (not future)+        --period-sort EXPR sort each report period's entries by EXPR+    -L, --actual           consider only actual (non-automated) transactions+    -r, --related          calculate report using related transactions+        --budget           generate budget entries based on periodic entries+        --add-budget       show all transactions plus the budget+        --unbudgeted       show only unbudgeted transactions+        --forecast EXPR    generate forecast entries while EXPR is true+    -l, --limit EXPR       calculate only transactions matching EXPR+    -t, --amount EXPR      use EXPR to calculate the displayed amount+    -T, --total EXPR       use EXPR to calculate the displayed total+    +    Output customization:+    -n, --collapse         Only show totals in the top-most accounts.+    -s, --subtotal         other: show subtotals+    -P, --by-payee         show summarized totals by payee+    -x, --comm-as-payee    set commodity name as the payee, for reporting+        --dow              show a days-of-the-week report+    -S, --sort EXPR        sort report according to the value expression EXPR+    -w, --wide             for the default register report, use 132 columns+        --head COUNT       show only the first COUNT entries (negative inverts)+        --tail COUNT       show only the last COUNT entries (negative inverts)+        --pager PAGER      send all output through the given PAGER program+    -A, --average          report average transaction amount+    -D, --deviation        report deviation from the average+    -%, --percentage       report balance totals as a percentile of the parent+        --totals           in the "xml" report, include running total+    -j, --amount-data      print only raw amount data (useful for scripting)+    -J, --total-data       print only raw total data+    -y, --date-format STR  use STR as the date format (default: %Y/%m/%d)+    -F, --format STR       use STR as the format; for each report type, use:+        --balance-format      --register-format       --print-format+        --plot-amount-format  --plot-total-format     --equity-format+        --prices-format       --wide-register-format+    +    Commodity reporting:+        --price-db FILE    sets the price database to FILE (def: ~/.pricedb)+    -L, --price-exp MINS   download quotes only if newer than MINS (def: 1440)+    -Q, --download         download price information when needed+    -O, --quantity         report commodity totals (this is the default)+    -V, --market           report last known market value+    -g, --performance      report gain/loss for each displayed transaction+    -G, --gain             report net gain/loss+    +    Commands:+    xml      [REGEXP]...   print matching entries in XML format+    equity   [REGEXP]...   output equity entries for matching accounts+    prices   [REGEXP]...   display price history for matching commodities+    entry DATE PAYEE AMT   output a derived entry, based on the arguments++#### Other differences++-   hledger recognises description and negative patterns by "desc:"+    and "not:" prefixes, unlike ledger 3's free-form parser+-   hledger doesn't require a space before command-line option+    values, you can write -f-+-   hledger's weekly reporting intervals always start on mondays+-   hledger shows start and end dates of the intervals requested,+    not just the span containing data+-   hledger period expressions don't support "biweekly",+    "bimonthly", or "every N days/weeks/..."+-   hledger always shows timelog balances in hours+-   hledger splits multi-day timelog sessions at midnight+-   hledger doesn't track the value of commodities with varying+    price; prices are fixed as of the transaction date+-   hledger print shows amounts for all postings, and shows unit+    prices for amounts which have them+-   hledger assumes a transaction's actual date comes first, and is+    required, while the optional effective date comes second (cf+    [Actual and effective dates](#actual-and-effective-dates))+-   hledger does not support per-posting actual or effective dates++### More examples and recipes++-   Here's a bash function that will run hledger chart and display+    the image in your (graphical) emacs:++        function chart () {+          hledger chart $* && emacsclient -n hledger.png+        }++    Example:++        $ chart food --depth 2 -p jan+
+ NEWS.rst view
@@ -0,0 +1,344 @@+---+title: hledger news+---+hledger news+============++2010/05/23 hledger 0.10+........................++  * fix too-loose testpack dependency, missing safe dependency+  * fix ghc 6.12 compatibility with -fweb+  * fix handling of non-ascii arguments with ghc 6.12+  * fix "0.8" in --version output+  * fix an occasional stack overflow error due to infinite recursion in Posting/Transaction equality tests+  * the -fwebhappstack build flag is gone for now, to avoid a cabal problem+  * parsing: if there is no description, don't require a space after the transaction date+  * parsing: balance balanced-virtual postings separately, allow them to have an implicit amount+  * parsing: timelog entries now generate balanced transactions, using virtual postings+  * parsing: simpler high-level parse error message+  * parsing: clearer bad date errors+  * add: fix wrongful program exit on bad dates+  * print: negative account patterns now exclude transactions containing any posting to a matched account+  * vty: rename the ui command to vty for consistency+  * vty: fix restricted account scope when backing up to top level+  * web: fix non-ascii handling with ghc 6.12+  * web: fix a bug possibly affecting reload-on-change+  * consolidate module namespace under Hledger, api cleanups++  Stats:+  44 days, 81 commits since last release.+  Now at 4904 lines of code including tests, 144 tests, 53% coverage.++2010/04/10 hledger 0.9+......................++  * ghc 6.12 support+  * split off hledger-lib package, containing core types & utils+  * parsing: ignore D, C, N, tag, end tag directives; we should now accept any ledger 2.6 file+  * parsing: allow numbers in commodities if double-quoted, like ledger+  * parsing: allow transactions with empty descriptions+  * parsing: show a better error for illegal month/day numbers in dates+  * parsing: don't ignore trailing junk in a smart date, eg in web add form+  * parsing: don't ignore unparsed text following an amount+  * parsing: @ was being treated as a currency symbol+  * add: fix precision handling in default amounts (#19)+  * add: elide last amount in added transactions+  * convert: keep original description by default, allow backreferences in replace pattern+  * convert: basic csv file checking, warn instead of dying when it looks wrong+  * convert: allow blank/comment lines at end of rules file+  * print: always show zero amounts as 0, hiding any commodity/decimal places/price, like ledger+  * register: fix bad layout with years < 1000+  * register: fix a Prelude.head error with reporting interval, --empty, and --depth+  * register: fix a regression, register should not show posting comments+  * register: with --empty, intervals should continue to ends of the specified period+  * stats: better output when last transaction is in the future+  * stats: show commodity symbols, account tree depth, reorder slightly+  * web: -fweb now builds with simpleserver; to get happstack, use -fwebhappstack instead+  * web: pre-fill the add form with today's date+  * web: help links, better search form wording+  * web: show a proper error for a bad date in add form (#17)+  * web: fix for unicode search form values+  * web: fix stack overflow caused by regexpr, and handle requests faster (#14)+  * web: look for more-generic browser executables+  * web: more robust browser starting (#6)+  * error message cleanups+  * more tests, refactoring, docs++  Stats:+  58 days, 2 contributors, 102 commits since last release.+  Now at 3983 lines of non-test code, 139 tests, 53% coverage.++2010/02/11 hledger 0.8+......................++  * parsing: in date=date2, use first date's year as a default for the second+  * add: ctrl-d doesn't work on windows, suggest ctrl-c instead+  * add: --no-new-accounts option disallows new accounts (Roman Cheplyaka)+  * add: re-use the previous transaction's date as default (Roman Cheplyaka)+  * add: a command-line argument now filters by account during history matching (Roman Cheplyaka)+  * chart: new command, generates balances pie chart (requires -fchart flag, gtk2hs) (Roman Cheplyaka, Simon Michael)+  * register: make reporting intervals honour a display expression (#18)+  * web: fix help link+  * web: use today as default when adding with a blank date+  * web: re-enable account/period fields, they seem to be fixed, along with file re-reading (#16)+  * web: get static files from the cabal data dir, or the current dir when using make (#13)+  * web: preserve encoding during add, assuming it's utf-8 (#15)+  * fix some non-utf8-aware file handling (#15)+  * filter ledger again for each command, not just once at program start+  * refactoring, clearer data types++  Stats:+  62 days, 2 contributors, 76 commits since last release.+  Now at 3464 lines of non-test code, 97 tests, 53% test coverage.++2009/12/11 hledger 0.7+........................++  * price history support (first cut):+    P directives now work, though differently from c++ ledger. Each+    posting amount takes its fixed unit price from the price history (or+    @) when available. This is simple and useful for things like foreign+    currency expenses (but not investment tracking). Like ledger, balance+    and register don't show amount prices any more, and don't separate+    differently-priced amounts. Unlike ledger, print shows all amount+    prices, and supports -B.+  * --effective option, will use transactions' effective dates if any+  * convert: new rules file format, find/create rules file automatically,+    more robust parsing, more useful --debug output+  * print: always sort by date, fix long account name truncation, align+    amounts, show end of line comments, show all amounts for clarity+    (don't elide the final balancing amount)+  * ui: use vty 4, fixes non-ascii and gnome terminal problems (issues #3, #4)+  * web: allow data entry, react to data file changes, better layout, help+    links, remove histogram command and filter fields for now, fix bad+    localhost redirect, filter form did not work in eg firefox (issue #7),+    reset link did not work in all browsers+  * parsing: require whitespace between date and status code, allow (and+    ignore) a time in price records, better error messages, non-zero exit+    code on parse failure+  * display non-ascii error messages properly (issue #5)+  * fix an arithmetic bug that occasionally rejected valid transactions+  * fix a regex bug in showtree+  * don't break if HOME is undefined+  * --debug now implies --verbose+  * add functional tests like ledger's, use test-framework for speedy+    running, release shelltestrunner as a separate package+  * many hlint cleanups (Marko Kocić)+  * many site and documentation updates++  Stats:+  60 days, 1 contributor, 50 commits since last release.+  Now at 3377 lines of non-test code, 97 tests, 53% test coverage.++2009/06/22 hledger 0.6.1+........................++  * avoid use of exitSuccess which was breaking ghc 6.8/base 3 compatibility (issue #2)++2009/06/13 hledger 0.6+......................++  * now cabal-installable on unix, mac, and windows, with Haskell Platform+  * provide experimental platform binaries+  * parsing: fix a silly failure to open ledger file paths containing ~+  * parsing: show better errors for unbalanced transaction and missing default year+  * parsing: allow parentheses and brackets inside account names, as ledger does+  * parsing: fail on empty account name components, don't just ignore+  * add: description passed as arguments now affects first transaction only+  * add: better handling of virtual postings and default amounts+  * print, register: show virtual accounts bracketed/parenthesised+  * web: improved web ui supporting full patterns & period expressions+  * new "stats" command reports some ledger statistics+  * many dev/doc/deployment infrastructure improvements+  * move website into darcs repo, update home page+  * move issue tracker to google code++Release stats:++  * Contributors: Simon Michael+  * Days since last release: 21+  * Commits: 94+  * Lines of non-test code: 2865+  * Tests: 82+  * Test coverage: 53% expressions+  * Known errors: 3 (inconsistent eliding, vty-related failures)+  * Performance: similar (http://hledger.org/profs/200906131120.bench)++2009/05/23 hledger 0.5.1+.................................++  * two fixes: really disable vty flag by default, and include ConvertCommand in cabal file++2009/05/23 hledger 0.5+...............................++  * the vty flag is disabled by default again, to ease installation on windows+  * use ledger 3 terminology: a ledger contains transactions which contain postings+  * new "add" command prompts for transactions interactively and adds them to the ledger+  * new "convert" command transforms bank CSV exports to ledger format, with rule-based cleanup+  * new "histogram" command shows transaction counts per day or other reporting interval+  * most commands now work properly with UTF8-encoded text (Sergey Astanin)+  * invoking as "hours" is now less different: it just uses your timelog, not your ledger+  * ..quarterly/-Q option summarises by quarter+  * ..uncleared/-U option looks only at uncleared transactions+  * be more accurate about checking balanced amounts, don't rely on display precision+  * enforce balancing for bracketed virtual postings+  * fix bug in eliding of posting amounts+  * don't show trailing spaces on amountless postings+  * parse null input as an empty ledger+  * don't treat comments as part of transaction descriptions+  * require some postings in ledger transactions+  * require a non-empty description in ledger transactions+  * don't fail when matching an empty pattern, as in "not:"+  * make the web server handle the null path+  * code, api and documentation updates+  * add a contributor agreement/list++Release stats:++  * Contributors: Simon Michael, Sergey Astanin+  * Days since last release: 51+  * Commits: 101+  * Lines of non-test code: 2795+  * Tests: 76+  * Known errors: 0++..+  * Performance:+                              || hledger-0.4 | hledger-0.5 | ledger+     =========================++=============+=============+=======+     -f sample.ledger balance ||        0.01 |        0.01 |   0.06+     -f 1000.ledger balance   ||        1.33 |        1.46 |   0.53+     -f 10000.ledger balance  ||       15.28 |       16.35 |   4.67+++2009/04/03 hledger 0.4+...............................++  * new "web" command serves reports in a web browser (install with -f happs to build this)+  * make the vty-based curses ui a cabal build option, which will be ignored on MS windows+  * drop the ..options-anywhere flag, that is now the default+  * patterns now use not: and desc: prefixes instead of ^ and ^^+  * patterns are now case-insensitive, like ledger+  * !include directives are now relative to the including file (Tim Docker)+  * "Y2009" default year directives are now supported, allowing m/d dates in ledger+  * individual transactions now have a cleared status+  * unbalanced entries now cause a proper warning+  * balance report now passes all ledger compatibility tests+  * balance report now shows subtotals by default, like ledger 3+  * balance report shows the final zero total when -E is used+  * balance report hides the final total when ..no-total is used+  * ..depth affects print and register reports (aggregating with a reporting interval, filtering otherwise)+  * register report sorts transactions by date+  * register report shows zero-amount transactions when -E is used+  * provide more convenient timelog querying when invoked as "hours"+  * multi-day timelog sessions are split at midnight+  * unterminated timelog sessions are now counted. Accurate time reports at last!+  * the test command gives better ..verbose output+  * ..version gives more detailed version numbers including patchlevel for dev builds+  * new make targets include: ghci, haddocktest, doctest, unittest, view-api-docs+  * a doctest-style framework for functional/shell tests has been added++Release stats:++  * Contributors: Simon Michael, Tim Docker; thanks to the HAppS, happstack and testpack developers+  * Days since release: 76+  * Commits: 144+  * Lines of non-test code: 2367+  * Tests: 56+  * Known errors: 0++..+  * Performance:+                                   || hledger-0.3 | hledger-0.4 | ledger-0.3+     ==============================++=============+=============+===========+     -f sample.ledger balance      ||        0.02 |        0.01 |       0.07+     -f sample1000.ledger balance  ||        1.02 |        1.39 |       0.53+     -f sample10000.ledger balance ||       12.72 |       14.97 |       4.63+++2009/01/17 hledger 0.3+...............................++  * count timelog sessions on the day they end, like ledger, for now+  * when options are repeated, use the last instead of the first+  * builds with ghc 6.10 as well as 6.8+  * a simple ui for interactive report browsing: hledger ui+  * accept smart dates everywhere (YYYYMMDD, Y/M/D, Y, M/D, D, jan, today, last week etc.)+  * ..period/-p flag accepting period expressions like "in 2008", "weekly from last month"..+  * -W/-M/-Y convenience flags to summarise register weekly, monthly, yearly+  * ..depth and -E flags also affect summarised register reports (including depth=0)+  * ..display/-d flag supporting date predicates (like "d<[DATE]", "d>=[DATE]")+  * !include directive to include additional ledger files+  * !account directive to set a default parent account+  * Added support for reading historical prices from files+  * timelog and ledger entries can be intermixed in one file+  * modifier and periodic entries can appear anywhere (but are still ignored)+  * help and readme improvements+  * runs much faster than 0.2++Release stats:++  * Contributors: Simon Michael, Nick Ingolia, Tim Docker; thanks to Corey O'Connor & the vty team+  * Lines of non-test code: 2123+  * Tests: 58+  * Known errors: 1++..+  * Performance:+     $ bench hledger-0.2 hledger ledger+                                       || hledger-0.2 | hledger | ledger+     ==================================++=============+=========+=======+     -f 2008.ledger -s balance         ||        2.59 |    0.26 |   0.11+     -f 10000entries.ledger -s balance ||      566.68 |    2.72 |   0.96+++2008/11/23 hledger 0.2+...............................++  * fix balance report totals when filtering by account+  * fix balance report selection of accounts when filtering by account+  * fix a bug with account name eliding in balance report+  * if we happen to be showing a not-yet-auto-balanced entry, hide the AUTO marker+  * fix print command filtering by account+  * omit transactions with zero amount from register report+  * Fix bug in parsing of timelogs+  * rename ..showsubs to ..subtotal, like ledger+  * drop ..usage flag+  * don't require quickcheck+  * priced amounts (eg "10h @ $50") and ..basis/..cost/-B flag to show them with cost basis+  * easy ..depth option, equivalent to c++ ledger's -d 'l<=N'+  * smarter y/m/d date parsing for -b and -e+    (any number of digits, month and day default to 1, separator can be / - or .)+  * -n flag for balance command+  * ..empty/-E flag+  * build a library, as well as the exe+  * new home page url (http://joyful.com/hledger)+  * publish html and pdf versions of README+  * detect display preferences for each commodity like c++ ledger+  * support amounts with multiple currencies/commodities+  * support ..real/-R flag+  * support -C/..cleared flag to filter by entry status (not transaction status)+  * support virtual and balanced virtual transactions+  * parse comment lines beginning with a space, as from M-; in emacs ledger-mode+  * allow any non-whitespace in account names, perhaps avoiding misleading missing amounts errors+  * clearer error message when we can't balance an entry+  * when we fail because of more than one missing amount in an entry, show the full entry+  * document the built-in test runner in ..help+  * add a ..verbose/-v flag, use it to show more test-running detail++Release stats:++  * Contributors: Simon Michael, Tim Docker+  * Lines of non-test code: 1350+  * Tests: 43+  * Known errors: 0+++2008/10/15 hledger 0.1+...............................++Release stats:++  * Contributors: Simon Michael
− Options.hs
@@ -1,277 +0,0 @@-{-# LANGUAGE CPP #-}-{-|-Command-line options for the application.--}--module Options -where-import System.Console.GetOpt-import System.Environment-import Ledger.IO (myLedgerPath,myTimelogPath)-import Ledger.Utils-import Ledger.Types-import Ledger.Dates-#if __GLASGOW_HASKELL__ <= 610-import Codec.Binary.UTF8.String (decodeString)-#endif-import Control.Monad (liftM)--progname      = "hledger"-timeprogname  = "hours"-#ifdef CHART-chartoutput   = "hledger.png"-chartitems    = 10-chartsize     = "600x400"-#endif--usagehdr =-  "Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]\n" ++-  "       hledger [OPTIONS] convert CSVFILE\n" ++-  "       hledger [OPTIONS] stats\n" ++-  "\n" ++-  "hledger uses your ~/.ledger or $LEDGER file, or another specified with -f\n" ++-  "\n" ++-  "COMMAND is one of (may be abbreviated):\n" ++-  "  add       - prompt for new transactions and add them to the ledger\n" ++-  "  balance   - show accounts, with balances\n" ++-  "  convert   - read CSV bank data and display in ledger format\n" ++-  "  histogram - show a barchart of transactions per day or other interval\n" ++-  "  print     - show transactions in ledger format\n" ++-  "  register  - show transactions as a register with running balance\n" ++-  "  stats     - show various statistics for a ledger\n" ++-#ifdef VTY-  "  ui        - run a simple text-based UI\n" ++-#endif-#ifdef WEB-  "  web       - run a simple web-based UI\n" ++-#endif-#ifdef CHART-  "  chart     - generate balances pie chart\n" ++-#endif-  "  test      - run self-tests\n" ++-  "\n" ++-  "PATTERNS are regular expressions which filter by account name.\n" ++-  "Prefix with desc: to filter by transaction description instead.\n" ++-  "Prefix with not: to negate a pattern. When using both, not: comes last.\n" ++-  "\n" ++-  "DATES can be y/m/d or ledger-style smart dates like \"last month\".\n" ++-  "\n" ++-  "Options:"--usageftr = ""-usage = usageInfo usagehdr options ++ usageftr---- | Command-line options we accept.-options :: [OptDescr Opt]-options = [-  Option "f" ["file"]         (ReqArg File "FILE")   "use a different ledger/timelog file; - means stdin"- ,Option ""  ["no-new-accounts"] (NoArg NoNewAccts)   "don't allow to create new accounts"- ,Option "b" ["begin"]        (ReqArg Begin "DATE")  "report on transactions on or after this date"- ,Option "e" ["end"]          (ReqArg End "DATE")    "report on transactions before this date"- ,Option "p" ["period"]       (ReqArg Period "EXPR") ("report on transactions during the specified period\n" ++-                                                       "and/or with the specified reporting interval\n")- ,Option "C" ["cleared"]      (NoArg  Cleared)       "report only on cleared transactions"- ,Option "U" ["uncleared"]    (NoArg  UnCleared)     "report only on uncleared transactions"- ,Option "B" ["cost","basis"] (NoArg  CostBasis)     "report cost of commodities"- ,Option ""    ["depth"]        (ReqArg Depth "N")     "hide accounts/transactions deeper than this"- ,Option "d" ["display"]      (ReqArg Display "EXPR") ("show only transactions matching EXPR (where\n" ++-                                                        "EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)")- ,Option ""    ["effective"]    (NoArg  Effective)     "use transactions' effective dates, if any"- ,Option "E" ["empty"]        (NoArg  Empty)         "show empty/zero things which are normally elided"- ,Option "R" ["real"]         (NoArg  Real)          "report only on real (non-virtual) transactions"- ,Option ""    ["no-total"]     (NoArg  NoTotal)       "balance report: hide the final total"--- ,Option "s" ["subtotal"]     (NoArg  SubTotal)      "balance report: show subaccounts"- ,Option "W" ["weekly"]       (NoArg  WeeklyOpt)     "register report: show weekly summary"- ,Option "M" ["monthly"]      (NoArg  MonthlyOpt)    "register report: show monthly summary"- ,Option "Q" ["quarterly"]    (NoArg  QuarterlyOpt)  "register report: show quarterly summary"- ,Option "Y" ["yearly"]       (NoArg  YearlyOpt)     "register report: show yearly summary"- ,Option "h" ["help"] (NoArg  Help)                  "show this help"- ,Option "V" ["version"]      (NoArg  Version)       "show version information"- ,Option "v" ["verbose"]      (NoArg  Verbose)       "show verbose test output"- ,Option ""    ["binary-filename"] (NoArg BinaryFilename) "show the download filename for this hledger build"- ,Option ""    ["debug"]        (NoArg  Debug)         "show extra debug output; implies verbose"- ,Option ""    ["debug-no-ui"]  (NoArg  DebugNoUI)     "run ui commands with no output"-#ifdef CHART- ,Option "o" ["output"]  (ReqArg ChartOutput "FILE")    ("chart: output filename (default: "++chartoutput++")")- ,Option ""  ["items"]  (ReqArg ChartItems "N")         ("chart: number of accounts to show (default: "++show chartitems++")")- ,Option ""  ["size"] (ReqArg ChartSize "WIDTHxHEIGHT") ("chart: image size (default: "++chartsize++")")-#endif- ]---- | An option value from a command-line flag.-data Opt = -    File    {value::String} | -    NoNewAccts |-    Begin   {value::String} | -    End     {value::String} | -    Period  {value::String} | -    Cleared | -    UnCleared | -    CostBasis | -    Depth   {value::String} | -    Display {value::String} | -    Effective | -    Empty | -    Real | -    NoTotal |-    SubTotal |-    WeeklyOpt |-    MonthlyOpt |-    QuarterlyOpt |-    YearlyOpt |-    Help |-    Verbose |-    Version-    | BinaryFilename-    | Debug-    | DebugNoUI-#ifdef CHART-    | ChartOutput {value::String}-    | ChartItems  {value::String}-    | ChartSize   {value::String}-#endif-    deriving (Show,Eq)---- these make me nervous-optsWithConstructor f opts = concatMap get opts-    where get o = [o | f v == o] where v = value o--optsWithConstructors fs opts = concatMap get opts-    where get o = [o | any (== o) fs]--optValuesForConstructor f opts = concatMap get opts-    where get o = [v | f v == o] where v = value o--optValuesForConstructors fs opts = concatMap get opts-    where get o = [v | any (\f -> f v == o) fs] where v = value o---- | Parse the command-line arguments into options, command name, and--- command arguments. Any dates in the options are converted to explicit--- YYYY/MM/DD format based on the current time.-parseArguments :: IO ([Opt], String, [String])-parseArguments = do-#if __GLASGOW_HASKELL__ <= 610-  args <- liftM (map decodeString) getArgs-#else-  args <- getArgs-#endif-  let (os,as,es) = getOpt Permute options args---  istimequery <- usingTimeProgramName---  let os' = if istimequery then (Period "today"):os else os-  os' <- fixOptDates os-  let os'' = if Debug `elem` os' then Verbose:os' else os'-  case (as,es) of-    (cmd:args,[])   -> return (os'',cmd,args)-    ([],[])         -> return (os'',"",[])-    (_,errs)        -> ioError (userError (concat errs ++ usage))---- | Convert any fuzzy dates within these option values to explicit ones,--- based on today's date.-fixOptDates :: [Opt] -> IO [Opt]-fixOptDates opts = do-  d <- getCurrentDay-  return $ map (fixopt d) opts-  where-    fixopt d (Begin s)   = Begin $ fixSmartDateStr d s-    fixopt d (End s)     = End $ fixSmartDateStr d s-    fixopt d (Display s) = -- hacky-        Display $ gsubRegexPRBy "\\[.+?\\]" fixbracketeddatestr s-        where fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]"-    fixopt _ o            = o---- | Figure out the overall date span we should report on, based on any--- begin/end/period options provided. If there is a period option, the--- others are ignored.-dateSpanFromOpts :: Day -> [Opt] -> DateSpan-dateSpanFromOpts refdate opts-    | not $ null popts = snd $ parsePeriodExpr refdate $ last popts-    | otherwise = DateSpan lastb laste-    where-      popts = optValuesForConstructor Period opts-      bopts = optValuesForConstructor Begin opts-      eopts = optValuesForConstructor End opts-      lastb = listtomaybeday bopts-      laste = listtomaybeday eopts-      listtomaybeday vs = if null vs then Nothing else Just $ parse $ last vs-          where parse = parsedate . fixSmartDateStr refdate---- | Figure out the reporting interval, if any, specified by the options.--- If there is a period option, the others are ignored.-intervalFromOpts :: [Opt] -> Interval-intervalFromOpts opts =-    case (periodopts, intervalopts) of-      ((p:_), _)            -> fst $ parsePeriodExpr d p where d = parsedate "0001/01/01" -- unused-      (_, (WeeklyOpt:_))    -> Weekly-      (_, (MonthlyOpt:_))   -> Monthly-      (_, (QuarterlyOpt:_)) -> Quarterly-      (_, (YearlyOpt:_))    -> Yearly-      (_, _)                -> NoInterval-    where-      periodopts   = reverse $ optValuesForConstructor Period opts-      intervalopts = reverse $ filter (`elem` [WeeklyOpt,MonthlyOpt,QuarterlyOpt,YearlyOpt]) opts---- | Get the value of the (last) depth option, if any, otherwise a large number.-depthFromOpts :: [Opt] -> Maybe Int-depthFromOpts opts = listtomaybeint $ optValuesForConstructor Depth opts-    where-      listtomaybeint [] = Nothing-      listtomaybeint vs = Just $ read $ last vs---- | Get the value of the (last) display option, if any.-displayExprFromOpts :: [Opt] -> Maybe String-displayExprFromOpts opts = listtomaybe $ optValuesForConstructor Display opts-    where-      listtomaybe [] = Nothing-      listtomaybe vs = Just $ last vs---- | Get a maybe boolean representing the last cleared/uncleared option if any.-clearedValueFromOpts opts | null os = Nothing-                          | last os == Cleared = Just True-                          | otherwise = Just False-    where os = optsWithConstructors [Cleared,UnCleared] opts---- | Was the program invoked via the \"hours\" alias ?-usingTimeProgramName :: IO Bool-usingTimeProgramName = do-  progname <- getProgName-  return $ map toLower progname == timeprogname---- | Get the ledger file path from options, an environment variable, or a default-ledgerFilePathFromOpts :: [Opt] -> IO String-ledgerFilePathFromOpts opts = do-  istimequery <- usingTimeProgramName-  f <- if istimequery then myTimelogPath else myLedgerPath-  return $ last $ f : optValuesForConstructor File opts---- | Gather filter pattern arguments into a list of account patterns and a--- list of description patterns. We interpret pattern arguments as--- follows: those prefixed with "desc:" are description patterns, all--- others are account patterns; also patterns prefixed with "not:" are--- negated. not: should come after desc: if both are used.-parsePatternArgs :: [String] -> ([String],[String])-parsePatternArgs args = (as, ds')-    where-      descprefix = "desc:"-      (ds, as) = partition (descprefix `isPrefixOf`) args-      ds' = map (drop (length descprefix)) ds---- | Convert application options to the library's generic filter specification.-optsToFilterSpec :: [Opt] -> [String] -> LocalTime -> FilterSpec-optsToFilterSpec opts args t = FilterSpec {-                                datespan=dateSpanFromOpts (localDay t) opts-                               ,cleared=clearedValueFromOpts opts-                               ,real=Real `elem` opts-                               ,empty=Empty `elem` opts-                               ,costbasis=CostBasis `elem` opts-                               ,acctpats=apats-                               ,descpats=dpats-                               ,whichdate = if Effective `elem` opts then EffectiveDate else ActualDate-                               ,depth = depthFromOpts opts-                               }-    where (apats,dpats) = parsePatternArgs args---- currentLocalTimeFromOpts opts = listtomaybe $ optValuesForConstructor CurrentLocalTime opts---     where---       listtomaybe [] = Nothing---       listtomaybe vs = Just $ last vs-
− README
@@ -1,36 +0,0 @@-hledger-=======--hledger is a computer program for easily tracking money, time, or other-commodities, using standard accounting principles. It is quite limited in-features, but reliable.  For some, it is a bare-bones, less complex, less-expensive alternative to Quicken or Microsoft Money.--hledger aims to help both computer experts and every-day users gain clarity in their finances and time management.-I use it every day to: --- track spending and income-- see time reports by day/week/month/project-- get accurate numbers for client billing and tax filing-- track invoices--Here is a demo_ of the web interface.--Here are ready-to-run binaries_ for mac, windows and gnu/linux.-They are not always the `latest release`_, sorry about that.--Here is the manual_.-For support and more technical info, see `hledger for techies`_ or `email me`_.--.. (If you're reading this in plain text, see also README2, MANUAL etc., or http://hledger.org)--.. _hledger for techies:  README2.html-.. _manual:               MANUAL.html-.. _demo:                 http://demo.hledger.org-.. _binaries:             http://hledger.org/binaries/-.. _hledger for mac:      http://hledger.org/binaries/hledger-0.6-mac-i386.gz-.. _hledger for windows:  http://hledger.org/binaries/hledger-0.6-win-i386.zip-.. _32 bit intel:         http://hledger.org/binaries/hledger-0.6.1+9-linux-i386.gz-.. _64 bit intel:         http://hledger.org/binaries/hledger-0.6-linux-x86_64.gz-.. _email me:             mailto:simon@joyful.com-.. _latest release:       http://hledger.org/MANUAL.html#installing
+ README.rst view
@@ -0,0 +1,39 @@+---+title: hledger+---+hledger+=======++hledger is a computer program for easily tracking money, time, or other+commodities, using standard accounting principles. It is quite limited in+features, but reliable.  For some, it is a bare-bones, less complex, less+expensive alternative to Quicken or Microsoft Money.++hledger aims to help both computer experts and every-day users gain clarity in their finances and time management.+I use it every day to: ++- track spending and income+- see time reports by day/week/month/project+- get accurate numbers for client billing and tax filing+- track invoices++Here is a demo_ of the web interface.++Here are ready-to-run binaries_ for mac, windows and gnu/linux.+They are not always the `latest release`_, sorry about that.++Here is the manual_.+For support and more technical info, see `hledger for techies`_ or `email me`_.++.. (If you're reading this in plain text, see also README2, MANUAL etc., or http://hledger.org)++.. _hledger for techies:  README2.html+.. _manual:               MANUAL.html+.. _demo:                 http://demo.hledger.org+.. _binaries:             http://hledger.org/binaries/+.. _hledger for mac:      http://hledger.org/binaries/hledger-0.6-mac-i386.gz+.. _hledger for windows:  http://hledger.org/binaries/hledger-0.6-win-i386.zip+.. _32 bit intel:         http://hledger.org/binaries/hledger-0.6.1+9-linux-i386.gz+.. _64 bit intel:         http://hledger.org/binaries/hledger-0.6-linux-x86_64.gz+.. _email me:             mailto:simon@joyful.com+.. _latest release:       http://hledger.org/MANUAL.html#installing
+ README2.rst view
@@ -0,0 +1,99 @@+---+title: hledger for techies+---+hledger for techies+===================++hledger_ is a remix, in haskell_, of John Wiegley's excellent ledger_ accounting tool.+It reads a plain text `ledger file`_ or timelog_ describing your transactions+and displays reports via `command line`_, curses_ or `web interface`_ (click for a demo).++The hledger project aims to produce:++- a practical, accessible, dependable tool for end users+- a useful library and toolbox for finance-minded haskell programmers+- a successful, time-and-money-solvent project within a thriving ecosystem of financial software projects.++hledger is free software by `Simon Michael`_ & `co.`_, released under GNU GPLv3.++**Learn**+ news_, manual_, screenshots_, `some extra tips`_++**Download**+ ``cabal install hledger``, + or try these ready-to-run binaries_,+ or see the `installing docs <MANUAL.html#installing>`_++**Develop**+ ``darcs get http://joyful.com/repos/hledger``, + `browse the repo`_, + `hackage page`_, + `hledger-lib api docs`_, + `hledger api docs`_, + `hledger-lib sourcegraph report`_, + `hledger sourcegraph report`_, + benchmark_\/profile_\/heap_\/coverage_ reports,+ `developer notes`_++.. raw:: html++ <a name="support" />++**Support**++- chat Simon (sm) on the `#ledger`_ irc channel which we share, or `email me`_+- report problems at `bugs.hledger.org <http://bugs.hledger.org>`_+- share and test ledger snippets at paste . hledger.org+- .. raw:: html++     <form action="http://groups.google.com/group/hledger/boxsubscribe" >+       join the hledger mail list at <a href="http://list.hledger.org">list.hledger.org</a>. Your email:+       <input type=text name=email><input type=submit name="sub" value="Subscribe">+     </form>++**Related projects**++- John Wiegley's ledger_ inspired hledger, and we try to stay compatible. You can often use both tools on the same ledger file.+- Uwe Hollerbach's umm_ is another haskell tool inspired by h/ledger.+- Tim Docker's ledger-reports_ uses hledger as a library to generate `html reports`_. +- I have a few older bits and pieces `here <http://joyful.com/Ledger>`_.++.. raw:: html++ <a href="http://joyful.com/darcsweb/darcsweb.cgi?r=hledger;a=shortlog"><img src=http://joyful.com/repos/hledger/commits.png border=0></a>+ <a href="https://www.google.com/analytics/reporting/?reset=1&id=15489822" accesskey="a"></a>+++.. _hledger:              README.html+.. _`ledger file`:        http://joyful.com/repos/hledger/sample.ledger+.. _timelog:              http://joyful.com/repos/hledger/sample.timelog+.. _command line:         SCREENSHOTS.html#hledger-screen-1+.. _curses:               SCREENSHOTS.html#sshot+.. _web interface:        http://demo.hledger.org+.. _mail list:            http://list.hledger.org+.. _issue tracker:        http://bugs.hledger.org+.. _binaries:             http://hledger.org/binaries/+.. _manual:               MANUAL.html+.. _news:                 NEWS.html+.. _screenshots:          SCREENSHOTS.html+.. _hledger-lib api docs: http://joyful.com/repos/hledger/hledger-lib/dist/doc/html/hledger-lib/index.html+.. _hledger api docs:     http://joyful.com/repos/hledger/dist/doc/html/hledger/index.html+.. _hledger-lib sourcegraph report: http://joyful.com/repos/hledger/hledger-lib/SourceGraph/hledger-lib.html+.. _hledger sourcegraph report: http://joyful.com/repos/hledger/SourceGraph/hledger.html+.. _developer notes:      http://joyful.com/darcsweb/darcsweb.cgi?r=hledger;a=plainblob;f=/NOTES+.. _benchmark:            http://hledger.org/profs/latest.bench+.. _profile:              http://hledger.org/profs/latest.prof+.. _heap:                 http://hledger.org/profs/latest.ps+.. _coverage:             http://hledger.org/profs/coverage/hpc_index_fun.html+.. _browse the repo:      http://joyful.com/darcsweb/darcsweb.cgi?r=hledger+.. _email me:             mailto:simon@joyful.com+.. _Simon Michael:        http://joyful.com+.. _co.:                  http://hledger.org/CONTRIBUTORS.html+.. _hackage page:         http://hackage.haskell.org/package/hledger+.. _#ledger:              irc://irc.freenode.net/#ledger+.. _haskell:              http://haskell.org+.. _ledger:               http://wiki.github.com/jwiegley/ledger+.. _umm:                  http://www.korgwal.com/umm/+.. _ledger-reports:       http://dockerz.net/repos/ledger-reports+.. _html reports:         http://dockerz.net/software/hledger_report_sample/report.html+.. _some extra tips:      http://podcastle.org/2009/10/09/pc-miniature-38-accounting-for-dragons/
− Tests.hs
@@ -1,1114 +0,0 @@-{- |--This module contains hledger's unit tests. These are built in to hledger,-and can be run at any time by doing @hledger test@ (or, with a few more-options, by doing @make unittest@ in the hledger source tree.)--Other kinds of tests:--hledger's functional tests are a set of shell/command-line tests defined-by .test files in the tests\/ subdirectory. These can be run by doing-@make functest@ in the hledger source tree.--hledger's doctests are shell tests defined in literal blocks in haddock-documentation in the source, run by doing @make doctest@ in the hledger-source tree. They are no longer used, but here is an example:--@-$ hledger -f sample.ledger balance o-                  $1  expenses:food-                 $-2  income-                 $-1    gifts-                 $-1    salary----------------------                 $-1-@---}--module Tests-where-import qualified Data.Map as Map-import Data.Time.Format-import System.Locale (defaultTimeLocale)-import Test.HUnit.Tools (runVerboseTests)-import System.Exit (exitFailure, exitWith, ExitCode(ExitSuccess)) -- base 3 compatible-import System.Time (ClockTime(TOD))--import Commands.All-import Ledger  -- including testing utils in Ledger.Utils-import Options-import Utils----- | Run unit tests.-runtests :: [Opt] -> [String] -> IO ()-runtests opts args = do-  (counts,_) <- runner ts-  if errors counts > 0 || (failures counts > 0)-   then exitFailure-   else exitWith ExitSuccess-    where-      runner | Verbose `elem` opts = runVerboseTests-             | otherwise = liftM (flip (,) 0) . runTestTT-      ts = TestList $ filter matchname $ tflatten tests  -- show flat test names-      -- ts = tfilter matchname $ TestList tests -- show hierarchical test names-      matchname = matchpats args . tname---- | unit tests, augmenting the ones defined in each module. Where that is--- inconvenient due to import cycles or whatever, we define them here.-tests :: Test-tests = TestList [-   tests_Ledger,-   tests_Commands,--   "account directive" ~:-   let sameParse str1 str2 = do l1 <- journalFromString str1-                                l2 <- journalFromString str2-                                l1 `is` l2-   in TestList-   [-    "account directive 1" ~: sameParse -                          "2008/12/07 One\n  test:from  $-1\n  test:to  $1\n"-                          "!account test\n2008/12/07 One\n  from  $-1\n  to  $1\n"--   ,"account directive 2" ~: sameParse -                           "2008/12/07 One\n  test:foo:from  $-1\n  test:foo:to  $1\n"-                           "!account test\n!account foo\n2008/12/07 One\n  from  $-1\n  to  $1\n"--   ,"account directive 3" ~: sameParse -                           "2008/12/07 One\n  test:from  $-1\n  test:to  $1\n"-                           "!account test\n!account foo\n!end\n2008/12/07 One\n  from  $-1\n  to  $1\n"--   ,"account directive 4" ~: sameParse -                           ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" ++-                            "!account outer\n2008/12/07 Two\n  aigh  $-2\n  bee  $2\n" ++-                            "!account inner\n2008/12/07 Three\n  gamma  $-3\n  delta  $3\n" ++-                            "!end\n2008/12/07 Four\n  why  $-4\n  zed  $4\n" ++-                            "!end\n2008/12/07 Five\n  foo  $-5\n  bar  $5\n"-                           )-                           ("2008/12/07 One\n  alpha  $-1\n  beta  $1\n" ++-                            "2008/12/07 Two\n  outer:aigh  $-2\n  outer:bee  $2\n" ++-                            "2008/12/07 Three\n  outer:inner:gamma  $-3\n  outer:inner:delta  $3\n" ++-                            "2008/12/07 Four\n  outer:why  $-4\n  outer:zed  $4\n" ++-                            "2008/12/07 Five\n  foo  $-5\n  bar  $5\n"-                           )-   ]--  ,"accountnames" ~:-    accountnames ledger7 `is`-     ["assets","assets:cash","assets:checking","assets:saving","equity","equity:opening balances",-      "expenses","expenses:food","expenses:food:dining","expenses:phone","expenses:vacation",-      "liabilities","liabilities:credit cards","liabilities:credit cards:discover"]--  ,"accountNameTreeFrom" ~: do-    accountNameTreeFrom ["a"]       `is` Node "top" [Node "a" []]-    accountNameTreeFrom ["a","b"]   `is` Node "top" [Node "a" [], Node "b" []]-    accountNameTreeFrom ["a","a:b"] `is` Node "top" [Node "a" [Node "a:b" []]]-    accountNameTreeFrom ["a:b:c"]   `is` Node "top" [Node "a" [Node "a:b" [Node "a:b:c" []]]]--  ,"balance report tests" ~:-   let (opts,args) `gives` es = do -        l <- sampleledgerwithopts opts args-        t <- getCurrentLocalTime-        showBalanceReport opts (optsToFilterSpec opts args t) l `is` unlines es-   in TestList-   [--    "balance report with no args" ~:-    ([], []) `gives`-    ["                 $-1  assets"-    ,"                  $1    bank:saving"-    ,"                 $-2    cash"-    ,"                  $2  expenses"-    ,"                  $1    food"-    ,"                  $1    supplies"-    ,"                 $-2  income"-    ,"                 $-1    gifts"-    ,"                 $-1    salary"-    ,"                  $1  liabilities:debts"-    ]--   ,"balance report can be limited with --depth" ~:-    ([Depth "1"], []) `gives`-    ["                 $-1  assets"-    ,"                  $2  expenses"-    ,"                 $-2  income"-    ,"                  $1  liabilities"-    ]-    -   ,"balance report with account pattern o" ~:-    ([SubTotal], ["o"]) `gives`-    ["                  $1  expenses:food"-    ,"                 $-2  income"-    ,"                 $-1    gifts"-    ,"                 $-1    salary"-    ,"--------------------"-    ,"                 $-1"-    ]--   ,"balance report with account pattern o and --depth 1" ~:-    ([Depth "1"], ["o"]) `gives`-    ["                  $1  expenses"-    ,"                 $-2  income"-    ,"--------------------"-    ,"                 $-1"-    ]--   ,"balance report with account pattern a" ~:-    ([], ["a"]) `gives`-    ["                 $-1  assets"-    ,"                  $1    bank:saving"-    ,"                 $-2    cash"-    ,"                 $-1  income:salary"-    ,"                  $1  liabilities:debts"-    ,"--------------------"-    ,"                 $-1"-    ]--   ,"balance report with account pattern e" ~:-    ([], ["e"]) `gives`-    ["                 $-1  assets"-    ,"                  $1    bank:saving"-    ,"                 $-2    cash"-    ,"                  $2  expenses"-    ,"                  $1    food"-    ,"                  $1    supplies"-    ,"                 $-2  income"-    ,"                 $-1    gifts"-    ,"                 $-1    salary"-    ,"                  $1  liabilities:debts"-    ]--   ,"balance report with unmatched parent of two matched subaccounts" ~: -    ([], ["cash","saving"]) `gives`-    ["                 $-1  assets"-    ,"                  $1    bank:saving"-    ,"                 $-2    cash"-    ,"--------------------"-    ,"                 $-1"-    ]--   ,"balance report with multi-part account name" ~: -    ([], ["expenses:food"]) `gives`-    ["                  $1  expenses:food"-    ,"--------------------"-    ,"                  $1"-    ]--   ,"balance report with negative account pattern" ~:-    ([], ["not:assets"]) `gives`-    ["                  $2  expenses"-    ,"                  $1    food"-    ,"                  $1    supplies"-    ,"                 $-2  income"-    ,"                 $-1    gifts"-    ,"                 $-1    salary"-    ,"                  $1  liabilities:debts"-    ,"--------------------"-    ,"                  $1"-    ]--   ,"balance report negative account pattern always matches full name" ~: -    ([], ["not:e"]) `gives` []--   ,"balance report negative patterns affect totals" ~: -    ([], ["expenses","not:food"]) `gives`-    ["                  $1  expenses:supplies"-    ,"--------------------"-    ,"                  $1"-    ]--   ,"balance report with -E shows zero-balance accounts" ~:-    ([SubTotal,Empty], ["assets"]) `gives`-    ["                 $-1  assets"-    ,"                  $1    bank"-    ,"                  $0      checking"-    ,"                  $1      saving"-    ,"                 $-2    cash"-    ,"--------------------"-    ,"                 $-1"-    ]--   ,"balance report with cost basis" ~: do-      j <- journalFromString $ unlines-             [""-             ,"2008/1/1 test           "-             ,"  a:b          10h @ $50"-             ,"  c:d                   "-             ,""-             ]-      let j' = canonicaliseAmounts True j -- enable cost basis adjustment-      showBalanceReport [] nullfilterspec nullledger{journal=j'} `is`-       unlines-        ["                $500  a:b"-        ,"               $-500  c:d"-        ]--   ,"balance report elides zero-balance root account(s)" ~: do-      l <- ledgerFromStringWithOpts []-             (unlines-              ["2008/1/1 one"-              ,"  test:a  1"-              ,"  test:b"-              ])-      showBalanceReport [] nullfilterspec l `is`-       unlines-        ["                   1  test:a"-        ,"                  -1  test:b"-        ]--   ]--  ,"balanceTransaction" ~: do-     assertBool "detect unbalanced entry, sign error"-                    (isLeft $ balanceTransaction-                           (Transaction (parsedate "2007/01/28") Nothing False "" "test" ""-                            [Posting False "a" (Mixed [dollars 1]) "" RegularPosting Nothing, -                             Posting False "b" (Mixed [dollars 1]) "" RegularPosting Nothing-                            ] ""))-     assertBool "detect unbalanced entry, multiple missing amounts"-                    (isLeft $ balanceTransaction-                           (Transaction (parsedate "2007/01/28") Nothing False "" "test" ""-                            [Posting False "a" missingamt "" RegularPosting Nothing, -                             Posting False "b" missingamt "" RegularPosting Nothing-                            ] ""))-     let e = balanceTransaction (Transaction (parsedate "2007/01/28") Nothing False "" "test" ""-                           [Posting False "a" (Mixed [dollars 1]) "" RegularPosting Nothing, -                            Posting False "b" missingamt "" RegularPosting Nothing-                           ] "")-     assertBool "one missing amount should be ok" (isRight e)-     assertEqual "balancing amount is added" -                     (Mixed [dollars (-1)])-                     (case e of-                        Right e' -> (pamount $ last $ tpostings e')-                        Left _ -> error "should not happen")--  ,"cacheLedger" ~:-    length (Map.keys $ accountmap $ cacheLedger journal7) `is` 15--  ,"canonicaliseAmounts" ~:-   "use the greatest precision" ~:-    journalPrecisions (canonicaliseAmounts False $ journalWithAmounts ["1","2.00"]) `is` [2,2]--  ,"commodities" ~:-    commodities ledger7 `is` [Commodity {symbol="$", side=L, spaced=False, comma=False, precision=2}]--  ,"dateSpanFromOpts" ~: do-    let todaysdate = parsedate "2008/11/26"-    let gives = is . show . dateSpanFromOpts todaysdate-    [] `gives` "DateSpan Nothing Nothing"-    [Begin "2008", End "2009"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)"-    [Period "in 2008"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)"-    [Begin "2005", End "2007",Period "in 2008"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)"--  -- don't know what this should do-  -- ,"elideAccountName" ~: do-  --    (elideAccountName 50 "aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa"-  --     `is` "aa:aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa")-  --    (elideAccountName 20 "aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaa"-  --     `is` "aa:aa:aaaaaaaaaaaaaa")--  ,"entriesFromTimeLogEntries" ~: do-     today <- getCurrentDay-     now' <- getCurrentTime-     tz <- getCurrentTimeZone-     let now = utcToLocalTime tz now'-         nowstr = showtime now-         yesterday = prevday today-         clockin = TimeLogEntry In-         mktime d = LocalTime d . fromMaybe midnight . parseTime defaultTimeLocale "%H:%M:%S"-         showtime = formatTime defaultTimeLocale "%H:%M"-         assertEntriesGiveStrings name es ss = assertEqual name ss (map tdescription $ entriesFromTimeLogEntries now es)--     assertEntriesGiveStrings "started yesterday, split session at midnight"-                                  [clockin (mktime yesterday "23:00:00") ""]-                                  ["23:00-23:59","00:00-"++nowstr]-     assertEntriesGiveStrings "split multi-day sessions at each midnight"-                                  [clockin (mktime (addDays (-2) today) "23:00:00") ""]-                                  ["23:00-23:59","00:00-23:59","00:00-"++nowstr]-     assertEntriesGiveStrings "auto-clock-out if needed" -                                  [clockin (mktime today "00:00:00") ""] -                                  ["00:00-"++nowstr]-     let future = utcToLocalTime tz $ addUTCTime 100 now'-         futurestr = showtime future-     assertEntriesGiveStrings "use the clockin time for auto-clockout if it's in the future"-                                  [clockin future ""]-                                  [printf "%s-%s" futurestr futurestr]--  ,"expandAccountNames" ~:-    expandAccountNames ["assets:cash","assets:checking","expenses:vacation"] `is`-     ["assets","assets:cash","assets:checking","expenses","expenses:vacation"]--  ,"intervalFromOpts" ~: do-    let gives = is . intervalFromOpts-    [] `gives` NoInterval-    [WeeklyOpt] `gives` Weekly-    [MonthlyOpt] `gives` Monthly-    [QuarterlyOpt] `gives` Quarterly-    [YearlyOpt] `gives` Yearly-    [Period "weekly"] `gives` Weekly-    [Period "monthly"] `gives` Monthly-    [Period "quarterly"] `gives` Quarterly-    [WeeklyOpt, Period "yearly"] `gives` Yearly--  ,"isAccountNamePrefixOf" ~: do-    "assets" `isAccountNamePrefixOf` "assets" `is` False-    "assets" `isAccountNamePrefixOf` "assets:bank" `is` True-    "assets" `isAccountNamePrefixOf` "assets:bank:checking" `is` True-    "my assets" `isAccountNamePrefixOf` "assets:bank" `is` False--  ,"isTransactionBalanced" ~: do-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""-             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)-             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)-             ] ""-     assertBool "detect balanced" (isTransactionBalanced t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""-             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)-             ,Posting False "c" (Mixed [dollars (-1.01)]) "" RegularPosting (Just t)-             ] ""-     assertBool "detect unbalanced" (not $ isTransactionBalanced t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""-             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)-             ] ""-     assertBool "detect unbalanced, one posting" (not $ isTransactionBalanced t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""-             [Posting False "b" (Mixed [dollars 0]) "" RegularPosting (Just t)-             ] ""-     assertBool "one zero posting is considered balanced for now" (isTransactionBalanced t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""-             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)-             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)-             ,Posting False "d" (Mixed [dollars 100]) "" VirtualPosting (Just t)-             ] ""-     assertBool "virtual postings don't need to balance" (isTransactionBalanced t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""-             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)-             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)-             ,Posting False "d" (Mixed [dollars 100]) "" BalancedVirtualPosting (Just t)-             ] ""-     assertBool "balanced virtual postings need to balance among themselves" (not $ isTransactionBalanced t)-     let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" ""-             [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting (Just t)-             ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting (Just t)-             ,Posting False "d" (Mixed [dollars 100]) "" BalancedVirtualPosting (Just t)-             ,Posting False "e" (Mixed [dollars (-100)]) "" BalancedVirtualPosting (Just t)-             ] ""-     assertBool "balanced virtual postings need to balance among themselves (2)" (isTransactionBalanced t)--  ,"isSubAccountNameOf" ~: do-    "assets" `isSubAccountNameOf` "assets" `is` False-    "assets:bank" `isSubAccountNameOf` "assets" `is` True-    "assets:bank:checking" `isSubAccountNameOf` "assets" `is` False-    "assets:bank" `isSubAccountNameOf` "my assets" `is` False--  ,"default year" ~: do-    rl <- journalFromString defaultyear_ledger_str-    tdate (head $ jtxns rl) `is` fromGregorian 2009 1 1-    return ()--  ,"ledgerFile" ~: do-    assertBool "ledgerFile should parse an empty file" (isRight $ parseWithCtx emptyCtx ledgerFile "")-    r <- journalFromString "" -- don't know how to get it from ledgerFile-    assertBool "ledgerFile parsing an empty file should give an empty ledger" $ null $ jtxns r--  ,"normaliseMixedAmount" ~: do-     normaliseMixedAmount (Mixed []) ~?= Mixed [nullamt]--  ,"parsedate" ~: do-    parsedate "2008/02/03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1-    parsedate "2008-02-03" `is` parsetimewith "%Y/%m/%d" "2008/02/03" date1--  ,"period expressions" ~: do-    let todaysdate = parsedate "2008/11/26"-    let str `gives` result = show (parsewith (periodexpr todaysdate) str) `is` ("Right " ++ result)-    "from aug to oct"           `gives` "(NoInterval,DateSpan (Just 2008-08-01) (Just 2008-10-01))"-    "aug to oct"                `gives` "(NoInterval,DateSpan (Just 2008-08-01) (Just 2008-10-01))"-    "every day from aug to oct" `gives` "(Daily,DateSpan (Just 2008-08-01) (Just 2008-10-01))"-    "daily from aug"            `gives` "(Daily,DateSpan (Just 2008-08-01) Nothing)"-    "every week to 2009"        `gives` "(Weekly,DateSpan Nothing (Just 2009-01-01))"--  ,"print report tests" ~: TestList-  [--   "print expenses" ~:-   do -    let args = ["expenses"]-    l <- sampleledgerwithopts [] args-    t <- getCurrentLocalTime-    showTransactions (optsToFilterSpec [] args t) l `is` unlines-     ["2008/06/03 * eat & shop"-     ,"    expenses:food                $1"-     ,"    expenses:supplies            $1"-     ,"    assets:cash                 $-2"-     ,""-     ]--  , "print report with depth arg" ~:-   do -    l <- sampleledger-    t <- getCurrentLocalTime-    showTransactions (optsToFilterSpec [Depth "2"] [] t) l `is` unlines-      ["2008/01/01 income"-      ,"    income:salary           $-1"-      ,""-      ,"2008/06/01 gift"-      ,"    income:gifts           $-1"-      ,""-      ,"2008/06/03 * eat & shop"-      ,"    expenses:food                $1"-      ,"    expenses:supplies            $1"-      ,"    assets:cash                 $-2"-      ,""-      ,"2008/12/31 * pay off"-      ,"    liabilities:debts            $1"-      ,""-      ]--  ]--  ,"punctuatethousands 1" ~: punctuatethousands "" `is` ""--  ,"punctuatethousands 2" ~: punctuatethousands "1234567.8901" `is` "1,234,567.8901"--  ,"punctuatethousands 3" ~: punctuatethousands "-100" `is` "-100"--  ,"register report tests" ~:-  let registerdates = filter (not . null) .  map (strip . take 10) . lines-  in-  TestList-  [--   "register report with no args" ~:-   do -    l <- sampleledger-    showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines-     ["2008/01/01 income               assets:bank:checking             $1           $1"-     ,"                                income:salary                   $-1            0"-     ,"2008/06/01 gift                 assets:bank:checking             $1           $1"-     ,"                                income:gifts                    $-1            0"-     ,"2008/06/02 save                 assets:bank:saving               $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ,"2008/06/03 eat & shop           expenses:food                    $1           $1"-     ,"                                expenses:supplies                $1           $2"-     ,"                                assets:cash                     $-2            0"-     ,"2008/12/31 pay off              liabilities:debts                $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ]--  ,"register report with cleared option" ~:-   do -    let opts = [Cleared]-    l <- ledgerFromStringWithOpts opts sample_ledger_str-    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines-     ["2008/06/03 eat & shop           expenses:food                    $1           $1"-     ,"                                expenses:supplies                $1           $2"-     ,"                                assets:cash                     $-2            0"-     ,"2008/12/31 pay off              liabilities:debts                $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ]--  ,"register report with uncleared option" ~:-   do -    let opts = [UnCleared]-    l <- ledgerFromStringWithOpts opts sample_ledger_str-    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines-     ["2008/01/01 income               assets:bank:checking             $1           $1"-     ,"                                income:salary                   $-1            0"-     ,"2008/06/01 gift                 assets:bank:checking             $1           $1"-     ,"                                income:gifts                    $-1            0"-     ,"2008/06/02 save                 assets:bank:saving               $1           $1"-     ,"                                assets:bank:checking            $-1            0"-     ]--  ,"register report sorts by date" ~:-   do -    l <- ledgerFromStringWithOpts [] $ unlines-        ["2008/02/02 a"-        ,"  b  1"-        ,"  c"-        ,""-        ,"2008/01/01 d"-        ,"  e  1"-        ,"  f"-        ]-    registerdates (showRegisterReport [] (optsToFilterSpec [] [] t1) l) `is` ["2008/01/01","2008/02/02"]--  ,"register report with account pattern" ~:-   do-    l <- sampleledger-    showRegisterReport [] (optsToFilterSpec [] ["cash"] t1) l `is` unlines-     ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"-     ]--  ,"register report with account pattern, case insensitive" ~:-   do -    l <- sampleledger-    showRegisterReport [] (optsToFilterSpec [] ["cAsH"] t1) l `is` unlines-     ["2008/06/03 eat & shop           assets:cash                     $-2          $-2"-     ]--  ,"register report with display expression" ~:-   do -    l <- sampleledger-    let gives displayexpr = -            (registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l) `is`)-                where opts = [Display displayexpr]-    "d<[2008/6/2]"  `gives` ["2008/01/01","2008/06/01"]-    "d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"]-    "d=[2008/6/2]"  `gives` ["2008/06/02"]-    "d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"]-    "d>[2008/6/2]"  `gives` ["2008/06/03","2008/12/31"]--  ,"register report with period expression" ~:-   do -    l <- sampleledger    -    let periodexpr `gives` dates = do-          l' <- sampleledgerwithopts opts []-          registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l') `is` dates-              where opts = [Period periodexpr]-    ""     `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]-    "2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]-    "2007" `gives` []-    "june" `gives` ["2008/06/01","2008/06/02","2008/06/03"]-    "monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"]-    "quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"]-    let opts = [Period "yearly"]-    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines-     ["2008/01/01 - 2008/12/31         assets:bank:saving               $1           $1"-     ,"                                assets:cash                     $-2          $-1"-     ,"                                expenses:food                    $1            0"-     ,"                                expenses:supplies                $1           $1"-     ,"                                income:gifts                    $-1            0"-     ,"                                income:salary                   $-1          $-1"-     ,"                                liabilities:debts                $1            0"-     ]-    let opts = [Period "quarterly"]-    registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l) `is` ["2008/01/01","2008/04/01","2008/10/01"]-    let opts = [Period "quarterly",Empty]-    registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]--  ]--  , "register report with depth arg" ~:-   do -    l <- sampleledger-    let opts = [Depth "2"]-    showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines-     ["2008/01/01 income               income:salary                   $-1          $-1"-     ,"2008/06/01 gift                 income:gifts                    $-1          $-2"-     ,"2008/06/03 eat & shop           expenses:food                    $1          $-1"-     ,"                                expenses:supplies                $1            0"-     ,"                                assets:cash                     $-2          $-2"-     ,"2008/12/31 pay off              liabilities:debts                $1          $-1"-     ]--  ,"show dollars" ~: show (dollars 1) ~?= "$1.00"--  ,"show hours" ~: show (hours 1) ~?= "1.0h"--  ,"unicode in balance layout" ~: do-    l <- ledgerFromStringWithOpts []-      "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n"-    showBalanceReport [] (optsToFilterSpec [] [] t1) l `is` unlines-      ["                -100  актив:наличные"-      ,"                 100  расходы:покупки"]--  ,"unicode in register layout" ~: do-    l <- ledgerFromStringWithOpts []-      "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n"-    showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines-      ["2009/01/01 медвежья шкура       расходы:покупки                 100          100"-      ,"                                актив:наличные                 -100            0"]--  ,"smart dates" ~: do-    let gives = is . fixSmartDateStr (parsedate "2008/11/26")-    "1999-12-02"   `gives` "1999/12/02"-    "1999.12.02"   `gives` "1999/12/02"-    "1999/3/2"     `gives` "1999/03/02"-    "19990302"     `gives` "1999/03/02"-    "2008/2"       `gives` "2008/02/01"-    "20/2"         `gives` "0020/02/01"-    "1000"         `gives` "1000/01/01"-    "4/2"          `gives` "2008/04/02"-    "2"            `gives` "2008/11/02"-    "January"      `gives` "2008/01/01"-    "feb"          `gives` "2008/02/01"-    "today"        `gives` "2008/11/26"-    "yesterday"    `gives` "2008/11/25"-    "tomorrow"     `gives` "2008/11/27"-    "this day"     `gives` "2008/11/26"-    "last day"     `gives` "2008/11/25"-    "next day"     `gives` "2008/11/27"-    "this week"    `gives` "2008/11/24" -- last monday-    "last week"    `gives` "2008/11/17" -- previous monday-    "next week"    `gives` "2008/12/01" -- next monday-    "this month"   `gives` "2008/11/01"-    "last month"   `gives` "2008/10/01"-    "next month"   `gives` "2008/12/01"-    "this quarter" `gives` "2008/10/01"-    "last quarter" `gives` "2008/07/01"-    "next quarter" `gives` "2009/01/01"-    "this year"    `gives` "2008/01/01"-    "last year"    `gives` "2007/01/01"-    "next year"    `gives` "2009/01/01"---     "last wed"     `gives` "2008/11/19"---     "next friday"  `gives` "2008/11/28"---     "next january" `gives` "2009/01/01"--  ,"subAccounts" ~: do-    l <- liftM cacheLedger' sampleledger-    let a = ledgerAccount l "assets"-    map aname (ledgerSubAccounts l a) `is` ["assets:bank","assets:cash"]--  -- ,"summarisePostingsInDateSpan" ~: do-  --   let gives (b,e,depth,showempty,ps) =-  --           (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`)-  --   let ps =-  --           [-  --            nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}-  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [dollars 2]}-  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food",          lpamount=Mixed [dollars 4]}-  --           ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining",   lpamount=Mixed [dollars 8]}-  --           ]-  --   ("2008/01/01","2009/01/01",0,9999,False,[]) `gives` -  --    []-  --   ("2008/01/01","2009/01/01",0,9999,True,[]) `gives` -  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31"}-  --    ]-  --   ("2008/01/01","2009/01/01",0,9999,False,ts) `gives` -  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",          lpamount=Mixed [dollars 4]}-  --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining",   lpamount=Mixed [dollars 10]}-  --    ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]}-  --    ]-  --   ("2008/01/01","2009/01/01",0,2,False,ts) `gives` -  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [dollars 15]}-  --    ]-  --   ("2008/01/01","2009/01/01",0,1,False,ts) `gives` -  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [dollars 15]}-  --    ]-  --   ("2008/01/01","2009/01/01",0,0,False,ts) `gives` -  --    [-  --     nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [dollars 15]}-  --    ]--  ]--  --- fixtures/test data--date1 = parsedate "2008/11/26"-t1 = LocalTime date1 midday--sampleledger = ledgerFromStringWithOpts [] sample_ledger_str-sampleledgerwithopts opts _ = ledgerFromStringWithOpts opts sample_ledger_str--sample_ledger_str = unlines- ["; A sample ledger file."- ,";"- ,"; Sets up this account tree:"- ,"; assets"- ,";   bank"- ,";     checking"- ,";     saving"- ,";   cash"- ,"; expenses"- ,";   food"- ,";   supplies"- ,"; income"- ,";   gifts"- ,";   salary"- ,"; liabilities"- ,";   debts"- ,""- ,"2008/01/01 income"- ,"    assets:bank:checking  $1"- ,"    income:salary"- ,""- ,"2008/06/01 gift"- ,"    assets:bank:checking  $1"- ,"    income:gifts"- ,""- ,"2008/06/02 save"- ,"    assets:bank:saving  $1"- ,"    assets:bank:checking"- ,""- ,"2008/06/03 * eat & shop"- ,"    expenses:food      $1"- ,"    expenses:supplies  $1"- ,"    assets:cash"- ,""- ,"2008/12/31 * pay off"- ,"    liabilities:debts  $1"- ,"    assets:bank:checking"- ,""- ,""- ,";final comment"- ]--defaultyear_ledger_str = unlines- ["Y2009"- ,""- ,"01/01 A"- ,"    a  $1"- ,"    b"- ]--write_sample_ledger = writeFile "sample.ledger" sample_ledger_str--entry2_str = unlines- ["2007/01/27 * joes diner"- ,"    expenses:food:dining                      $10.00"- ,"    expenses:gifts                            $10.00"- ,"    assets:checking                          $-20.00"- ,""- ]--entry3_str = unlines- ["2007/01/01 * opening balance"- ,"    assets:cash                                $4.82"- ,"    equity:opening balances"- ,""- ,"2007/01/01 * opening balance"- ,"    assets:cash                                $4.82"- ,"    equity:opening balances"- ,""- ,"2007/01/28 coopportunity"- ,"  expenses:food:groceries                 $47.18"- ,"  assets:checking"- ,""- ]--periodic_entry1_str = unlines- ["~ monthly from 2007/2/2"- ,"  assets:saving            $200.00"- ,"  assets:checking"- ,""- ]--periodic_entry2_str = unlines- ["~ monthly from 2007/2/2"- ,"  assets:saving            $200.00         ;auto savings"- ,"  assets:checking"- ,""- ]--periodic_entry3_str = unlines- ["~ monthly from 2007/01/01"- ,"    assets:cash                                $4.82"- ,"    equity:opening balances"- ,""- ,"~ monthly from 2007/01/01"- ,"    assets:cash                                $4.82"- ,"    equity:opening balances"- ,""- ]--ledger1_str = unlines- [""- ,"2007/01/27 * joes diner"- ,"  expenses:food:dining                    $10.00"- ,"  expenses:gifts                          $10.00"- ,"  assets:checking                        $-20.00"- ,""- ,""- ,"2007/01/28 coopportunity"- ,"  expenses:food:groceries                 $47.18"- ,"  assets:checking                        $-47.18"- ,""- ,""- ]--ledger2_str = unlines- [";comment"- ,"2007/01/27 * joes diner"- ,"  expenses:food:dining                    $10.00"- ,"  assets:checking                        $-47.18"- ,""- ]--ledger3_str = unlines- ["2007/01/27 * joes diner"- ,"  expenses:food:dining                    $10.00"- ,";intra-entry comment"- ,"  assets:checking                        $-47.18"- ,""- ]--ledger4_str = unlines- ["!include \"somefile\""- ,"2007/01/27 * joes diner"- ,"  expenses:food:dining                    $10.00"- ,"  assets:checking                        $-47.18"- ,""- ]--ledger5_str = ""--ledger6_str = unlines- ["~ monthly from 2007/1/21"- ,"    expenses:entertainment  $16.23        ;netflix"- ,"    assets:checking"- ,""- ,"; 2007/01/01 * opening balance"- ,";     assets:saving                            $200.04"- ,";     equity:opening balances                         "- ,""- ]--ledger7_str = unlines- ["2007/01/01 * opening balance"- ,"    assets:cash                                $4.82"- ,"    equity:opening balances                         "- ,""- ,"2007/01/01 * opening balance"- ,"    income:interest                                $-4.82"- ,"    equity:opening balances                         "- ,""- ,"2007/01/02 * ayres suites"- ,"    expenses:vacation                        $179.92"- ,"    assets:checking                                 "- ,""- ,"2007/01/02 * auto transfer to savings"- ,"    assets:saving                            $200.00"- ,"    assets:checking                                 "- ,""- ,"2007/01/03 * poquito mas"- ,"    expenses:food:dining                       $4.82"- ,"    assets:cash                                     "- ,""- ,"2007/01/03 * verizon"- ,"    expenses:phone                            $95.11"- ,"    assets:checking                                 "- ,""- ,"2007/01/03 * discover"- ,"    liabilities:credit cards:discover         $80.00"- ,"    assets:checking                                 "- ,""- ,"2007/01/04 * blue cross"- ,"    expenses:health:insurance                 $90.00"- ,"    assets:checking                                 "- ,""- ,"2007/01/05 * village market liquor"- ,"    expenses:food:dining                       $6.48"- ,"    assets:checking                                 "- ,""- ]--journal7 = Journal-          [] -          [] -          [-           txnTieKnot $ Transaction {-             tdate=parsedate "2007/01/01",-             teffectivedate=Nothing,-             tstatus=False,-             tcode="*",-             tdescription="opening balance",-             tcomment="",-             tpostings=[-              Posting {-                pstatus=False,-                paccount="assets:cash",-                pamount=(Mixed [dollars 4.82]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              },-              Posting {-                pstatus=False,-                paccount="equity:opening balances",-                pamount=(Mixed [dollars (-4.82)]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              }-             ],-             tpreceding_comment_lines=""-           }-          ,-           txnTieKnot $ Transaction {-             tdate=parsedate "2007/02/01",-             teffectivedate=Nothing,-             tstatus=False,-             tcode="*",-             tdescription="ayres suites",-             tcomment="",-             tpostings=[-              Posting {-                pstatus=False,-                paccount="expenses:vacation",-                pamount=(Mixed [dollars 179.92]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              },-              Posting {-                pstatus=False,-                paccount="assets:checking",-                pamount=(Mixed [dollars (-179.92)]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              }-             ],-             tpreceding_comment_lines=""-           }-          ,-           txnTieKnot $ Transaction {-             tdate=parsedate "2007/01/02",-             teffectivedate=Nothing,-             tstatus=False,-             tcode="*",-             tdescription="auto transfer to savings",-             tcomment="",-             tpostings=[-              Posting {-                pstatus=False,-                paccount="assets:saving",-                pamount=(Mixed [dollars 200]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              },-              Posting {-                pstatus=False,-                paccount="assets:checking",-                pamount=(Mixed [dollars (-200)]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              }-             ],-             tpreceding_comment_lines=""-           }-          ,-           txnTieKnot $ Transaction {-             tdate=parsedate "2007/01/03",-             teffectivedate=Nothing,-             tstatus=False,-             tcode="*",-             tdescription="poquito mas",-             tcomment="",-             tpostings=[-              Posting {-                pstatus=False,-                paccount="expenses:food:dining",-                pamount=(Mixed [dollars 4.82]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              },-              Posting {-                pstatus=False,-                paccount="assets:cash",-                pamount=(Mixed [dollars (-4.82)]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              }-             ],-             tpreceding_comment_lines=""-           }-          ,-           txnTieKnot $ Transaction {-             tdate=parsedate "2007/01/03",-             teffectivedate=Nothing,-             tstatus=False,-             tcode="*",-             tdescription="verizon",-             tcomment="",-             tpostings=[-              Posting {-                pstatus=False,-                paccount="expenses:phone",-                pamount=(Mixed [dollars 95.11]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              },-              Posting {-                pstatus=False,-                paccount="assets:checking",-                pamount=(Mixed [dollars (-95.11)]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              }-             ],-             tpreceding_comment_lines=""-           }-          ,-           txnTieKnot $ Transaction {-             tdate=parsedate "2007/01/03",-             teffectivedate=Nothing,-             tstatus=False,-             tcode="*",-             tdescription="discover",-             tcomment="",-             tpostings=[-              Posting {-                pstatus=False,-                paccount="liabilities:credit cards:discover",-                pamount=(Mixed [dollars 80]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              },-              Posting {-                pstatus=False,-                paccount="assets:checking",-                pamount=(Mixed [dollars (-80)]),-                pcomment="",-                ptype=RegularPosting,-                ptransaction=Nothing-              }-             ],-             tpreceding_comment_lines=""-           }-          ]-          []-          []-          ""-          ""-          (TOD 0 0)-          ""--ledger7 = cacheLedger journal7--ledger8_str = unlines- ["2008/1/1 test           "- ,"  a:b          10h @ $40"- ,"  c:d                   "- ,""- ]--timelogentry1_str  = "i 2007/03/11 16:19:00 hledger\n"-timelogentry1 = TimeLogEntry In (parsedatetime "2007/03/11 16:19:00") "hledger"--timelogentry2_str  = "o 2007/03/11 16:30:00\n"-timelogentry2 = TimeLogEntry Out (parsedatetime "2007/03/11 16:30:00") ""--a1 = Mixed [(hours 1){price=Just $ Mixed [Amount (comm "$") 10 Nothing]}]-a2 = Mixed [(hours 2){price=Just $ Mixed [Amount (comm "EUR") 10 Nothing]}]-a3 = Mixed $ amounts a1 ++ amounts a2--journalWithAmounts :: [String] -> Journal-journalWithAmounts as =-        Journal-        []-        []-        [t | a <- as, let t = nulltransaction{tdescription=a,tpostings=[nullposting{pamount=parse a,ptransaction=Just t}]}]-        []-        []-        ""-        ""-        (TOD 0 0)-        ""-    where parse = fromparse . parseWithCtx emptyCtx postingamount . (" "++)-
− Utils.hs
@@ -1,94 +0,0 @@-{-# LANGUAGE CPP #-}-{-|--Utilities for top-level modules and ghci. See also "Ledger.IO" and-"Ledger.Utils".---}--module Utils-where-import Control.Monad.Error-import Ledger-import Options (Opt(..),ledgerFilePathFromOpts) -- ,optsToFilterSpec)-import System.Directory (doesFileExist)-import System.IO (stderr)-#if __GLASGOW_HASKELL__ <= 610-import System.IO.UTF8 (hPutStrLn)-#else-import System.IO (hPutStrLn)-#endif-import System.Exit-import System.Process (readProcessWithExitCode)-import System.Info (os)-import System.Time (ClockTime,getClockTime)----- | Parse the user's specified ledger file and run a hledger command on--- it, or report a parse error. This function makes the whole thing go.--- Warning, this provides only an uncached Ledger (no accountnametree or--- accountmap), so cmd must cacheLedger'/crunchJournal if needed.-withLedgerDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Ledger -> IO ()) -> IO ()-withLedgerDo opts args cmdname cmd = do-  -- We kludgily read the file before parsing to grab the full text, unless-  -- it's stdin, or it doesn't exist and we are adding. We read it strictly-  -- to let the add command work.-  f <- ledgerFilePathFromOpts opts-  let f' = if f == "-" then "/dev/null" else f-  fileexists <- doesFileExist f-  let creating = not fileexists && cmdname == "add"-  t <- getCurrentLocalTime-  tc <- getClockTime-  txt <-  if creating then return "" else strictReadFile f'-  let runcmd = cmd opts args . mkLedger opts f tc txt-  if creating-   then runcmd nulljournal-   else (runErrorT . parseLedgerFile t) f >>= either parseerror runcmd-    where parseerror e = hPutStrLn stderr e >> exitWith (ExitFailure 1)--mkLedger :: [Opt] -> FilePath -> ClockTime -> String -> Journal -> Ledger-mkLedger opts f tc txt j = nullledger{journal=j'}-    where j' = (canonicaliseAmounts costbasis j){filepath=f,filereadtime=tc,jtext=txt}-          costbasis=CostBasis `elem` opts---- | Get a Ledger from the given string and options, or raise an error.-ledgerFromStringWithOpts :: [Opt] -> String -> IO Ledger-ledgerFromStringWithOpts opts s = do-    tc <- getClockTime-    j <- journalFromString s-    return $ mkLedger opts "" tc s j---- -- | Read a Ledger from the given file, or give an error.--- readLedgerWithOpts :: [Opt] -> [String] -> FilePath -> IO Ledger--- readLedgerWithOpts opts args f = do---   t <- getCurrentLocalTime---   readLedger f-           --- -- | Convert a Journal to a canonicalised, cached and filtered Ledger--- -- based on the command-line options/arguments and a reference time.--- filterAndCacheLedgerWithOpts ::  [Opt] -> [String] -> LocalTime -> String -> Journal -> Ledger--- filterAndCacheLedgerWithOpts opts args = filterAndCacheLedger . optsToFilterSpec opts args---- | Attempt to open a web browser on the given url, all platforms.-openBrowserOn :: String -> IO ExitCode-openBrowserOn u = trybrowsers browsers u-    where-      trybrowsers (b:bs) u = do-        (e,_,_) <- readProcessWithExitCode b [u] ""-        case e of-          ExitSuccess -> return ExitSuccess-          ExitFailure _ -> trybrowsers bs u-      trybrowsers [] u = do-        putStrLn $ printf "Sorry, I could not start a browser (tried: %s)" $ intercalate ", " browsers-        putStrLn $ printf "Please open your browser and visit %s" u-        return $ ExitFailure 127-      browsers | os=="darwin"  = ["open"]-               | os=="mingw32" = ["start"]-               | otherwise     = ["sensible-browser","gnome-www-browser","firefox"]-    -- jeffz: write a ffi binding for it using the Win32 package as a basis-    -- start by adding System/Win32/Shell.hsc and follow the style of any-    -- other module in that directory for types, headers, error handling and-    -- what not.-    -- ::ShellExecute(NULL, "open", "www.somepage.com", NULL, NULL, SW_SHOWNORMAL);-    -- ::ShellExecute(NULL, "open", "firefox.exe", "www.somepage.com" NULL, SW_SHOWNORMAL);-
− Version.hs
@@ -1,75 +0,0 @@-{-# LANGUAGE CPP #-}-{--Version-related utilities. See the Makefile for details of our version-numbering policy.--}--module Version-where-import System.Info (os, arch)-import Ledger.Utils-import Options (progname)---- version and PATCHLEVEL are set by the makefile-version       = "0.8.0"--#ifdef PATCHLEVEL-patchlevel = "." ++ show PATCHLEVEL -- must be numeric !-#else-patchlevel = ""-#endif--buildversion  = version ++ patchlevel :: String--binaryfilename = prettify $ splitAtElement '.' buildversion :: String-                where-                  prettify (major:minor:bugfix:patches:[]) =-                      printf "hledger-%s.%s%s%s-%s-%s%s" major minor bugfix' patches' os' arch suffix-                          where-                            bugfix'-                                | bugfix `elem` ["0"{-,"98","99"-}] = ""-                                | otherwise = '.' : bugfix-                            patches'-                                | patches/="0" = '+' : patches-                                | otherwise = ""-                            (os',suffix)-                                | os == "darwin"  = ("mac","")-                                | os == "mingw32" = ("windows",".exe")-                                | otherwise       = (os,"")-                  prettify (major:minor:bugfix:[]) = prettify [major,minor,bugfix,"0"]-                  prettify (major:minor:[])        = prettify [major,minor,"0","0"]-                  prettify (major:[])              = prettify [major,"0","0","0"]-                  prettify []                      = error "VERSION is empty, please fix"-                  prettify _                       = error "VERSION has too many components, please fix"--versionstr    = prettify $ splitAtElement '.' buildversion :: String-                where-                  prettify (major:minor:bugfix:patches:[]) =-                      printf "%s.%s%s%s" major minor bugfix' patches'-                          where-                            bugfix'-                                | bugfix `elem` ["0"{-,"98","99"-}] = ""-                                | otherwise = '.' : bugfix-                            patches'-                                | patches/="0" = "+"++patches-                                | otherwise = ""-                  prettify s = intercalate "." s--versionmsg    = progname ++ "-" ++ versionstr ++ configmsg :: String-    where configmsg-              | null configflags = " with no extras"-              | otherwise = " with " ++ intercalate ", " configflags--configflags   = tail [""-#ifdef CHART-  ,"chart"-#endif-#ifdef VTY-  ,"vty"-#endif-#if defined(WEB)-  ,"web (using simpleserver)"-#elif defined(WEBHAPPSTACK)-  ,"web (using happstack)"-#endif- ]
+ data/sample.ledger view
@@ -0,0 +1,40 @@+; A sample ledger file.+;+; Sets up this account tree:+; assets+;   bank+;     checking+;     saving+;   cash+; expenses+;   food+;   supplies+; income+;   gifts+;   salary+; liabilities+;   debts++2008/01/01 income+    assets:bank:checking  $1+    income:salary++2008/06/01 gift+    assets:bank:checking  $1+    income:gifts++2008/06/02 save+    assets:bank:saving  $1+    assets:bank:checking++2008/06/03 * eat & shop+    expenses:food      $1+    expenses:supplies  $1+    assets:cash++2008/12/31 * pay off+    liabilities:debts  $1+    assets:bank:checking+++;final comment
+ data/sample.rules view
@@ -0,0 +1,17 @@+base-account assets:bank:checking+date-field 0+description-field 4+amount-field 1+currency $++# account-assigning rules++SPECTRUM+expenses:health:gym++ITUNES+BLKBSTR=BLOCKBUSTER+expenses:entertainment++(TO|FROM) SAVINGS+assets:bank:savings
+ data/sample.timelog view
@@ -0,0 +1,6 @@+i 2009/03/27 09:00:00 projects:a+o 2009/03/27 17:00:34+i 2009/03/31 22:21:45 personal:reading:online+o 2009/04/01 02:00:34+i 2009/04/02 09:00:00 projects:b+o 2009/04/02 17:00:34
hledger.cabal view
@@ -1,5 +1,5 @@ name:           hledger-version:        0.9+version: 0.10 category:       Finance synopsis:       A command-line (or curses or web-based) double-entry accounting tool. description:@@ -26,9 +26,14 @@                 web/style.css extra-tmp-files: extra-source-files:-  README-  sample.ledger-  sample.timelog+  README.rst+  README2.rst+  MANUAL.markdown+  NEWS.rst+  CONTRIBUTORS.rst+  data/sample.ledger+  data/sample.timelog+  data/sample.rules  flag vty   description: enable the curses ui@@ -38,48 +43,29 @@   description: enable the web ui (using simpleserver)   default:     False -flag webhappstack-  description: enable the web ui (using happstack)-  default:     False- flag chart   description: enable the pie chart generation   default:     False --- minimal library so cabal list and ghc-pkg list will show this package-library-  exposed-modules:-                  Version---   Build-Depends:---                   base >= 3 && < 5---                  ,containers---                  ,directory---                  ,filepath---                  ,old-time---                  ,parsec---                  ,time---                  ,utf8-string >= 0.3---                  ,HUnit- executable hledger   main-is:        hledger.hs   other-modules:-                  HledgerMain-                  Options                   Paths_hledger-                  Tests-                  Utils-                  Version-                  Commands.Add-                  Commands.All-                  Commands.Balance-                  Commands.Convert-                  Commands.Histogram-                  Commands.Print-                  Commands.Register-                  Commands.Stats+                  Hledger.Cli.Main+                  Hledger.Cli.Options+                  Hledger.Cli.Tests+                  Hledger.Cli.Utils+                  Hledger.Cli.Version+                  Hledger.Cli.Commands.Add+                  Hledger.Cli.Commands.All+                  Hledger.Cli.Commands.Balance+                  Hledger.Cli.Commands.Convert+                  Hledger.Cli.Commands.Histogram+                  Hledger.Cli.Commands.Print+                  Hledger.Cli.Commands.Register+                  Hledger.Cli.Commands.Stats   build-depends:-                  hledger-lib == 0.9+                  hledger-lib >= 0.10                  ,HUnit                  ,base >= 3 && < 5                  ,containers@@ -93,7 +79,7 @@                  ,process                  ,regexpr >= 0.5.1                  ,safe >= 0.2-                 ,testpack+                 ,testpack >= 1 && < 2                  ,time                  ,utf8-string >= 0.3 @@ -102,13 +88,13 @@    if flag(vty)     cpp-options: -DVTY-    other-modules:Commands.UI+    other-modules:Hledger.Cli.Commands.Vty     build-depends:                   vty >= 4.0.0.1    if flag(web)     cpp-options: -DWEB-    other-modules:Commands.Web+    other-modules:Hledger.Cli.Commands.Web     build-depends:                   hsp                  ,hsx@@ -121,10 +107,60 @@                  ,HTTP >= 4000.0                  ,applicative-extras -  if flag(webhappstack)-    cpp-options: -DWEBHAPPSTACK-    other-modules:Commands.Web+  if flag(chart)+    cpp-options: -DCHART+    other-modules:Hledger.Cli.Commands.Chart     build-depends:+                  Chart >= 0.11+                 ,colour++library+  exposed-modules:+                  Hledger.Cli.Main+                  Hledger.Cli.Options+                  Hledger.Cli.Tests+                  Hledger.Cli.Utils+                  Hledger.Cli.Version+                  Hledger.Cli.Commands.Add+                  Hledger.Cli.Commands.All+                  Hledger.Cli.Commands.Balance+                  Hledger.Cli.Commands.Convert+                  Hledger.Cli.Commands.Histogram+                  Hledger.Cli.Commands.Print+                  Hledger.Cli.Commands.Register+                  Hledger.Cli.Commands.Stats+  build-depends:+                  hledger-lib >= 0.10+                 ,HUnit+                 ,base >= 3 && < 5+                 ,containers+                 ,csv+                 ,directory+                 ,filepath+                 ,mtl+                 ,old-locale+                 ,old-time+                 ,parsec+                 ,process+                 ,regexpr >= 0.5.1+                 ,safe >= 0.2+                 ,testpack >= 1 && < 2+                 ,time+                 ,utf8-string >= 0.3++  -- should set patchlevel here as in Makefile+  cpp-options:    -DPATCHLEVEL=0++  if flag(vty)+    cpp-options: -DVTY+    exposed-modules:Hledger.Cli.Commands.Vty+    build-depends:+                  vty >= 4.0.0.1++  if flag(web)+    cpp-options: -DWEB+    exposed-modules:Hledger.Cli.Commands.Web+    build-depends:                   hsp                  ,hsx                  ,xhtml >= 3000.2@@ -132,17 +168,13 @@                  ,io-storage                  ,hack-contrib                  ,hack-                 ,hack-handler-happstack-                 ,happstack >= 0.3-                 ,happstack-data >= 0.3-                 ,happstack-server >= 0.3-                 ,happstack-state >= 0.3+                 ,hack-handler-simpleserver                  ,HTTP >= 4000.0                  ,applicative-extras    if flag(chart)     cpp-options: -DCHART-    other-modules:Commands.Chart+    exposed-modules:Hledger.Cli.Commands.Chart     build-depends:                   Chart >= 0.11                  ,colour
hledger.hs view
@@ -1,39 +1,4 @@ #!/usr/bin/env runhaskell-{-|-hledger - a ledger-compatible text-based accounting tool.--Copyright (c) 2007-2010 Simon Michael <simon@joyful.com>-Released under GPL version 3 or later.--hledger is a partial haskell clone of John Wiegley's "ledger" text-based-accounting tool.  It generates ledger-compatible register & balance-reports from a plain text journal, and demonstrates a functional-implementation of ledger.  For more information, see http:\/\/hledger.org .--You can use the command line:--> $ hledger --help--or ghci:--> $ ghci hledger-> > l <- readLedger "sample.ledger"-> > register [] ["income","expenses"] l-> 2008/01/01 income               income:salary                   $-1          $-1-> 2008/06/01 gift                 income:gifts                    $-1          $-2-> 2008/06/03 eat & shop           expenses:food                    $1          $-1->                                 expenses:supplies                $1            0-> > balance [Depth "1"] [] l->                  $-1  assets->                   $2  expenses->                  $-2  income->                   $1  liabilities-> > l <- myLedger-> > t <- myTimelog--See "Ledger.Ledger" for more examples.---}+-- the hledger command-line executable; see Hledger/Cli/Main.hs -module Main where-import HledgerMain (main)+import Hledger.Cli.Main (main)
− sample.ledger
@@ -1,40 +0,0 @@-; A sample ledger file.-;-; Sets up this account tree:-; assets-;   bank-;     checking-;     saving-;   cash-; expenses-;   food-;   supplies-; income-;   gifts-;   salary-; liabilities-;   debts--2008/01/01 income-    assets:bank:checking  $1-    income:salary--2008/06/01 gift-    assets:bank:checking  $1-    income:gifts--2008/06/02 save-    assets:bank:saving  $1-    assets:bank:checking--2008/06/03 * eat & shop-    expenses:food      $1-    expenses:supplies  $1-    assets:cash--2008/12/31 * pay off-    liabilities:debts  $1-    assets:bank:checking---;final comment
− sample.timelog
@@ -1,6 +0,0 @@-i 2009/03/27 09:00:00 projects:a-o 2009/03/27 17:00:34-i 2009/03/31 22:21:45 personal:reading:online-o 2009/04/01 02:00:34-i 2009/04/02 09:00:00 projects:b-o 2009/04/02 17:00:34