packages feed

hledger 0.15.2 → 0.16

raw patch · 9 files changed

+142/−121 lines, 9 filesdep ~hledger-libPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: hledger-lib

API changes (from Hackage documentation)

- Hledger.Cli.Add: appendToJournalFile :: FilePath -> String -> IO ()
- Hledger.Cli.Convert: inField :: CsvRules -> Maybe FieldPosition
- Hledger.Cli.Convert: outField :: CsvRules -> Maybe FieldPosition
- Hledger.Cli.Convert: printTxn :: Bool -> CsvRules -> CsvRecord -> IO ()
+ Hledger.Cli.Add: appendToJournalFileOrStdout :: FilePath -> String -> IO ()
+ Hledger.Cli.Add: ensureOneNewlineTerminated :: String -> String
+ Hledger.Cli.Convert: amountInField :: CsvRules -> Maybe FieldPosition
+ Hledger.Cli.Convert: amountOutField :: CsvRules -> Maybe FieldPosition

Files

Hledger/Cli.hs view
@@ -249,20 +249,6 @@         ,"                   0"         ] -   ,"balance report elides zero-balance root account(s)" ~: do-      j <- readJournal'-             (unlines-              ["2008/1/1 one"-              ,"  test:a  1"-              ,"  test:b"-              ])-      accountsReportAsText defreportopts (accountsReport defreportopts nullfilterspec j) `is`-        ["                   1  test:a"-        ,"                  -1  test:b"-        ,"--------------------"-        ,"                   0"-        ]-    ]    ,"journalCanonicaliseAmounts" ~:
Hledger/Cli/Add.hs view
@@ -51,10 +51,10 @@ add opts j     | f == "-" = return ()     | otherwise = do+  hPrintf stderr "Adding transactions to journal file \"%s\".\n" f   hPutStrLn stderr $-    "Enter one or more transactions, which will be added to your journal file.\n"-    ++"To complete a transaction, enter . when prompted for an account.\n"-    ++"To quit, press control-d or control-c."+    "To complete a transaction, enter . (period) at an account prompt.\n"+    ++"To stop adding transactions, enter . at a date prompt, or control-d/control-c."   today <- getCurrentDay   getAndAddTransactions j opts today         `catch` (\e -> unless (isEOFError e) $ ioError e)@@ -75,10 +75,12 @@                     -> IO (Transaction,Day) getTransaction j opts defaultDate = do   today <- getCurrentDay-  datestr <- runInteractionDefault $ askFor "date" +  datestr <- runInteractionDefault $ askFor "date, or . to end"             (Just $ showDate defaultDate)-            (Just $ \s -> null s || -             isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s))+            (Just $ \s -> null s+                         || s == "."+                         || isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s))+  when (datestr == ".") $ ioError $ mkIOError eofErrorType "" Nothing Nothing   description <- runInteractionDefault $ askFor "description" (Just "") Nothing   let historymatches = transactionsSimilarTo j (patterns_ $ reportopts_ opts) description       bestmatch | null historymatches = Nothing@@ -117,9 +119,15 @@                 | otherwise = Nothing                 where Just ps = historicalps       defaultaccount = maybe Nothing (Just . showacctname) bestmatch-  account <- runInteraction j $ askFor (printf "account %d" n) defaultaccount (Just accept)+      ordot | null enteredps || length enteredrealps == 1 = ""+            | otherwise = ", or . to record"+  account <- runInteraction j $ askFor (printf "account %d%s" n ordot) defaultaccount (Just accept)   if account=="."-    then return enteredps+    then+     if null enteredps+      then do hPutStrLn stderr $ "\nPlease enter some postings first."+              getPostings st enteredps+      else return enteredps     else do       let defaultacctused = Just account == defaultaccount           historicalps' = if defaultacctused then historicalps else Nothing@@ -131,10 +139,16 @@                            | n > 1             = Just balancingamountstr                            | otherwise         = Nothing               where-                -- force a decimal point in the output in case there's a-                -- digit group separator that would be mistaken for one-                historicalamountstr = showMixedAmountWithPrecision maxprecisionwithpoint $ pamount $ fromJust bestmatch'-                balancingamountstr  = showMixedAmountWithPrecision maxprecisionwithpoint $ negate $ sum $ map pamount enteredrealps+                historicalamountstr = showMixedAmountWithPrecision p $ pamount $ fromJust bestmatch'+                balancingamountstr  = showMixedAmountWithPrecision p $ negate $ sum $ map pamount enteredrealps+                -- what should this be ?+                -- 1 maxprecision (show all decimal places or none) ?+                -- 2 maxprecisionwithpoint (show all decimal places or .0 - avoids some but not all confusion with thousands separators) ?+                -- 3 canonical precision for this commodity in the journal ?+                -- 4 maximum precision entered so far in this transaction ?+                -- 5 3 or 4, whichever would show the most decimal places ?+                -- I think 1 or 4, whichever would show the most decimal places+                p = maxprecisionwithpoint       amountstr <- runInteractionDefault $ askFor (printf "amount  %d" n) defaultamountstr validateamount       let amount  = fromparse $ runParser (someamount <|> return missingamt) ctx     "" amountstr           amount' = fromparse $ runParser (someamount <|> return missingamt) nullctx "" amountstr@@ -191,26 +205,24 @@ journalAddTransaction :: Journal -> CliOpts -> Transaction -> IO Journal journalAddTransaction j@Journal{jtxns=ts} opts t = do   let f = journalFilePath j-  appendToJournalFile f $ showTransaction t+  appendToJournalFileOrStdout f $ showTransaction t   when (debug_ opts) $ do     putStrLn $ printf "\nAdded transaction to %s:" f     putStrLn =<< registerFromString (show t)   return j{jtxns=ts++[t]} --- | Append data to a journal file; or if the file is "-", dump it to stdout.-appendToJournalFile :: FilePath -> String -> IO ()-appendToJournalFile f s =-    if f == "-"-    then putStr $ sep ++ s-    else appendFile f $ sep++s-    where -      -- appendFile means we don't need file locking to be-      -- multi-user-safe, but also that we can't figure out the minimal-      -- number of newlines needed as separator-      sep = "\n\n"-      -- sep | null $ strip t = ""-      --     | otherwise = replicate (2 - min 2 (length lastnls)) '\n'-      --     where lastnls = takeWhile (=='\n') $ reverse t+-- | Append a string, typically one or more transactions, to a journal+-- file, or if the file is "-", dump it to stdout.  Tries to avoid+-- excess whitespace.+appendToJournalFileOrStdout :: FilePath -> String -> IO ()+appendToJournalFileOrStdout f s+  | f == "-"  = putStr s'+  | otherwise = appendFile f s'+  where s' = "\n" ++ ensureOneNewlineTerminated s++-- | Replace a string's 0 or more terminating newlines with exactly one.+ensureOneNewlineTerminated :: String -> String+ensureOneNewlineTerminated = (++"\n") . reverse . dropWhile (=='\n') . reverse  -- | Convert a string of journal data into a register report. registerFromString :: String -> IO String
Hledger/Cli/Balance.hs view
@@ -82,15 +82,14 @@ 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.+Also, non-interesting accounts may be elided.  Here's an imperfect+description of the ledger balance command's eliding behaviour:+\"Interesting\" accounts are displayed on their own line. An account less+deep than the report's max depth, with just one interesting subaccount,+and the same balance as the subaccount, is non-interesting, and prefixed+to the subaccount's line, unless (hledger's) --no-elide is in effect.+An account with a zero inclusive balance and less than two interesting+subaccounts is not displayed at all, unless --empty is in effect.  -} 
Hledger/Cli/Convert.hs view
@@ -5,7 +5,9 @@  module Hledger.Cli.Convert where import Control.Monad (when, guard, liftM)+import Data.List import Data.Maybe+import Data.Ord import Data.Time.Format (parseTime) import Safe import System.Directory (doesFileExist)@@ -14,7 +16,7 @@ import System.IO (stderr) import System.Locale (defaultTimeLocale) import Test.HUnit-import Text.CSV (parseCSV, parseCSVFromFile, printCSV, CSV)+import Text.CSV (parseCSV, parseCSVFromFile, CSV) import Text.ParserCombinators.Parsec import Text.Printf (hPrintf) @@ -37,8 +39,8 @@       codeField :: Maybe FieldPosition,       descriptionField :: [FormatString],       amountField :: Maybe FieldPosition,-      inField :: Maybe FieldPosition,-      outField :: Maybe FieldPosition,+      amountInField :: Maybe FieldPosition,+      amountOutField :: Maybe FieldPosition,       currencyField :: Maybe FieldPosition,       baseCurrency :: Maybe String,       accountField :: Maybe FieldPosition,@@ -55,8 +57,8 @@       codeField=Nothing,       descriptionField=[],       amountField=Nothing,-      inField=Nothing,-      outField=Nothing,+      amountInField=Nothing,+      amountOutField=Nothing,       currencyField=Nothing,       baseCurrency=Nothing,       accountField=Nothing,@@ -86,11 +88,11 @@       usingStdin = csvfile == "-"       rulesFileSpecified = isJust $ rules_file_ opts       rulesfile = rulesFileFor opts csvfile-  when (usingStdin && (not rulesFileSpecified)) $ error' "please use --rules to specify a rules file when converting stdin"+  when (usingStdin && (not rulesFileSpecified)) $ error' "please use --rules-file to specify a rules file when converting stdin"   csvparse <- parseCsv csvfile   let records = case csvparse of                   Left e -> error' $ show e-                  Right rs -> reverse $ filter (/= [""]) rs+                  Right rs -> filter (/= [""]) rs   exists <- doesFileExist rulesfile   if (not exists) then do                   hPrintf stderr "creating conversion rules file %s, edit this file for better results\n" rulesfile@@ -104,7 +106,8 @@   let requiredfields = max 2 (maxFieldIndex rules + 1)       badrecords = take 1 $ filter ((< requiredfields).length) records   if null badrecords-   then mapM_ (printTxn (debug_ opts) rules) records+   then do+     mapM_ (putStr . show) $ sortBy (comparing tdate) $ map (transactionFromCsvRecord rules) records    else do      hPrintf stderr (unlines [                       "Warning, at least one CSV record does not contain a field referenced by the"@@ -127,8 +130,8 @@                   ,statusField r                   ,codeField r                   ,amountField r-                  ,inField r-                  ,outField r+                  ,amountInField r+                  ,amountOutField r                   ,currencyField r                   ,accountField r                   ,account2Field r@@ -149,7 +152,7 @@     "date-field 0\n" ++     "description-field 4\n" ++     "amount-field 1\n" ++-    "currency $\n" +++    "base-currency $\n" ++     "\n" ++     "# account-assigning rules\n" ++     "\n" ++@@ -166,14 +169,14 @@ validateRules :: CsvRules -> Maybe String validateRules rules = let     hasAmount = isJust $ amountField rules-    hasIn = isJust $ inField rules-    hasOut = isJust $ outField rules+    hasIn = isJust $ amountInField rules+    hasOut = isJust $ amountOutField rules   in case (hasAmount, hasIn, hasOut) of-    (True, True, _) -> Just "Don't specify in-field when specifying amount-field"-    (True, _, True) -> Just "Don't specify out-field when specifying amount-field"-    (_, False, True) -> Just "Please specify in-field when specifying out-field"-    (_, True, False) -> Just "Please specify out-field when specifying in-field"-    (False, False, False) -> Just "Please specify either amount-field, or in-field and out-field"+    (True, True, _) -> Just "Don't specify amount-in-field when specifying amount-field"+    (True, _, True) -> Just "Don't specify amount-out-field when specifying amount-field"+    (_, False, True) -> Just "Please specify amount-in-field when specifying amount-out-field"+    (_, True, False) -> Just "Please specify amount-out-field when specifying amount-in-field"+    (False, False, False) -> Just "Please specify either amount-field, or amount-in-field and amount-out-field"     _ -> Nothing  -- rules file parser@@ -205,8 +208,8 @@    ,codefield    ,descriptionfield    ,amountfield-   ,infield-   ,outfield+   ,amountinfield+   ,amountoutfield    ,currencyfield    ,accountfield    ,account2field@@ -269,17 +272,17 @@   x <- updateState (\r -> r{amountField=readMay v})   return x -infield = do-  string "in-field"+amountinfield = do+  choice [string "amount-in-field", string "in-field"]   many1 spacenonewline   v <- restofline-  updateState (\r -> r{inField=readMay v})+  updateState (\r -> r{amountInField=readMay v}) -outfield = do-  string "out-field"+amountoutfield = do+  choice [string "amount-out-field", string "out-field"]   many1 spacenonewline   v <- restofline-  updateState (\r -> r{outField=readMay v})+  updateState (\r -> r{amountOutField=readMay v})  currencyfield = do   string "currency-field"@@ -300,7 +303,7 @@   updateState (\r -> r{account2Field=readMay v})  basecurrency = do-  string "currency"+  choice [string "base-currency", string "currency"]   many1 spacenonewline   v <- restofline   updateState (\r -> r{baseCurrency=Just v})@@ -340,11 +343,6 @@   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 formatD :: CsvRecord -> Bool -> Maybe Int -> Maybe Int -> Field -> String formatD record leftJustified min max f = case f of @@ -426,19 +424,37 @@ -- | Convert some date string with unknown format to YYYY/MM/DD. normaliseDate :: Maybe String -- ^ User-supplied date format: this should be tried in preference to all others               -> String -> String-normaliseDate mb_user_format s = maybe "0000/00/00" showDate $-              firstJust $-              (maybe id (\user_format -> (parseTime defaultTimeLocale user_format s :)) mb_user_format) $-              [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)-              ]+normaliseDate mb_user_format s =+    let parsewith = flip (parseTime defaultTimeLocale) s in+    maybe (error' $ "could not parse \""++s++"\" as a date, consider adding a date-format directive or upgrading")+          showDate $+          firstJust $ (map parsewith $+                       maybe [] (:[]) mb_user_format+                       -- the - modifier requires time-1.2.0.5, released+                       -- in 2011/5, so for now we emulate it for wider+                       -- compatibility.  time < 1.2.0.5 also has a buggy+                       -- %y which we don't do anything about.+                       -- ++ [+                       -- "%Y/%m/%d"+                       -- ,"%Y/%-m/%-d"+                       -- ,"%Y-%m-%d"+                       -- ,"%Y-%-m-%-d"+                       -- ,"%m/%d/%Y"+                       -- ,"%-m/%-d/%Y"+                       -- ,"%m-%d-%Y"+                       -- ,"%-m-%-d-%Y"+                       -- ]+                      )+                      ++ [+                       parseTime defaultTimeLocale "%Y/%m/%e" s+                      ,parseTime defaultTimeLocale "%Y-%m-%e" s+                      ,parseTime defaultTimeLocale "%m/%e/%Y" s+                      ,parseTime defaultTimeLocale "%m-%e-%Y" s+                      ,parseTime defaultTimeLocale "%Y/%m/%e" (take 5 s ++ "0" ++ drop 5 s)+                      ,parseTime defaultTimeLocale "%Y-%m-%e" (take 5 s ++ "0" ++ drop 5 s)+                      ,parseTime defaultTimeLocale "%m/%e/%Y" ('0':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.@@ -460,13 +476,13 @@ getAmount rules fields = case amountField rules of   Just f  -> maybe "" (atDef "" fields) $ Just f   Nothing ->-    case (c, d) of+    case (i, o) of       (x, "") -> x       ("", x) -> "-"++x-      _ -> ""+      p -> error' $ "using amount-in-field and amount-out-field, found a value in both fields: "++show p     where-      c = maybe "" (atDef "" fields) (inField rules)-      d = maybe "" (atDef "" fields) (outField rules)+      i = maybe "" (atDef "" fields) (amountInField rules)+      o = maybe "" (atDef "" fields) (amountOutField rules)  tests_Hledger_Cli_Convert = TestList (test_parser ++ test_description_parsing) 
Hledger/Cli/Main.hs view
@@ -31,7 +31,6 @@ >                  $-2  income >                   $1  liabilities > > l <- myLedger-> > t <- myTimelog  See "Hledger.Data.Ledger" for more examples. @@ -47,6 +46,7 @@ import System.Process import Text.Printf +import Hledger (ensureJournalFile) import Hledger.Cli.Add import Hledger.Cli.Balance import Hledger.Cli.Convert@@ -69,10 +69,11 @@   run' opts addons args     where       run' opts@CliOpts{command_=cmd} addons args-       | "version" `in_` (rawopts_ opts)                 = putStrLn progversion-       | "binary-filename" `in_` (rawopts_ opts)         = putStrLn $ binaryfilename progname+       -- delicate, add tests before changing (eg --version, ADDONCMD --version, INTERNALCMD --version)+       | (null matchedaddon) && "version" `in_` (rawopts_ opts)         = putStrLn progversion+       | (null matchedaddon) && "binary-filename" `in_` (rawopts_ opts) = putStrLn $ binaryfilename progname        | null cmd                                        = putStr $ showModeHelp mainmode'-       | cmd `isPrefixOf` "add"                          = showModeHelpOr addmode      $ withJournalDo opts add+       | cmd `isPrefixOf` "add"                          = showModeHelpOr addmode      $ journalFilePathFromOpts opts >>= ensureJournalFile >> withJournalDo opts add        | cmd `isPrefixOf` "convert"                      = showModeHelpOr convertmode  $ convert opts        | cmd `isPrefixOf` "test"                         = showModeHelpOr testmode     $ runtests opts        | any (cmd `isPrefixOf`) ["accounts","balance"]   = showModeHelpOr accountsmode $ withJournalDo opts balance@@ -80,15 +81,18 @@        | any (cmd `isPrefixOf`) ["postings","register"]  = showModeHelpOr postingsmode $ withJournalDo opts register        | any (cmd `isPrefixOf`) ["activity","histogram"] = showModeHelpOr activitymode $ withJournalDo opts histogram        | cmd `isPrefixOf` "stats"                        = showModeHelpOr statsmode    $ withJournalDo opts stats-       | not (null matchedaddon)                           = system shellcmd >>= exitWith+       | not (null matchedaddon)                           = do+                                                             when (debug_ opts) $ printf "running %s\n" shellcmd+                                                             system shellcmd >>= exitWith        | otherwise                                       = optserror ("command "++cmd++" is not recognized") >> exitFailure        where         mainmode' = mainmode addons         showModeHelpOr mode f | "help" `in_` (rawopts_ opts) = putStr $ showModeHelp mode                               | otherwise = f-        matchedaddon = headDef "" $ filter (cmd `isPrefixOf`) addons-        shellcmd = printf "%s-%s %s" progname matchedaddon (unwords' argswithoutcmd)-        argswithoutcmd = args1 ++ drop 1 args2 where (args1,args2) = break (== cmd) args+        matchedaddon | null cmd  = ""+                     | otherwise = headDef "" $ filter (cmd `isPrefixOf`) addons+        shellcmd = printf "%s-%s %s" progname matchedaddon (unwords' subcmdargs)+        subcmdargs = args1 ++ drop 1 args2 where (args1,args2) = break (== cmd) $ filter (/="--") args  {- tests: 
Hledger/Cli/Options.hs view
@@ -318,7 +318,7 @@ getHledgerCliOpts :: [String] -> IO CliOpts getHledgerCliOpts addons = do   args <- getArgs-  toCliOpts (decodeRawOpts $ processValue (mainmode addons) $ moveFileOption args) >>= checkCliOpts+  toCliOpts (decodeRawOpts $ processValue (mainmode addons) $ rearrangeForCmdArgs args) >>= checkCliOpts  -- utils @@ -344,12 +344,12 @@ -- | Convert possibly encoded option values to regular unicode strings. decodeRawOpts = map (\(name,val) -> (name, fromPlatformString val)) --- A workaround related to http://code.google.com/p/ndmitchell/issues/detail?id=457 :--- we'd like to permit options before COMMAND as well as after it. Here we--- make sure at least -f FILE will be accepted in either position.-moveFileOption (fopt@('-':'f':_:_):cmd:rest) = cmd:fopt:rest-moveFileOption ("-f":fval:cmd:rest) = cmd:"-f":fval:rest-moveFileOption as = as+-- A hacky workaround for http://code.google.com/p/ndmitchell/issues/detail?id=470 :+-- we'd like to permit options before COMMAND as well as after it.+-- Here we make sure at least -f FILE will be accepted in either position.+rearrangeForCmdArgs (fopt@('-':'f':_:_):cmd:rest) = cmd:fopt:rest+rearrangeForCmdArgs ("-f":fval:cmd:rest) = cmd:"-f":fval:rest+rearrangeForCmdArgs as = as  optserror = error' . (++ " (run with --help for usage)") @@ -423,11 +423,15 @@     , FormatField True Nothing Nothing Format.Account     ] --- | Get the journal file path from options, an environment variable, or a default+-- | Get the journal file path from options, an environment variable, or a default.+-- If the path contains a literal tilde raise an error to avoid confusion. journalFilePathFromOpts :: CliOpts -> IO String journalFilePathFromOpts opts = do   f <- myJournalPath-  return $ fromMaybe f $ file_ opts+  return $ errorIfContainsTilde $ fromMaybe f $ file_ opts++errorIfContainsTilde s |'~' `elem` s = error' "unsupported literal ~ found in environment variable, please adjust"+                       | otherwise   = s  aliasesFromOpts :: CliOpts -> [(AccountName,AccountName)] aliasesFromOpts = map parseAlias . alias_
Hledger/Cli/Print.hs view
@@ -25,5 +25,5 @@ showTransactions opts fspec j = entriesReportAsText opts fspec $ entriesReport opts fspec j  entriesReportAsText :: ReportOpts -> FilterSpec -> EntriesReport -> String-entriesReportAsText opts _ items = concatMap (showTransactionForPrint (effective_ opts)) items+entriesReportAsText _ _ items = concatMap showTransactionUnelided items 
Hledger/Cli/Version.hs view
@@ -20,7 +20,7 @@ -- version and PATCHLEVEL are set by the make process  version :: String-version       = "0.15.2"+version       = "0.16.0"  patchlevel :: String #ifdef PATCHLEVEL
hledger.cabal view
@@ -1,5 +1,5 @@ name:           hledger-version: 0.15.2+version: 0.16 category:       Finance synopsis:       The main command-line interface for the hledger accounting tool. description:@@ -57,7 +57,7 @@                   Hledger.Cli.Stats   -- should be the same as below   build-depends:-                  hledger-lib == 0.15.2+                  hledger-lib == 0.16                  ,base >= 3 && < 5                  ,containers                  ,cmdargs >= 0.8   && < 0.9@@ -107,7 +107,7 @@   ghc-options:    -threaded -W   -- should be the same as above   build-depends:-                  hledger-lib == 0.15.2+                  hledger-lib == 0.16                  ,base >= 3 && < 5                  ,containers                  ,cmdargs >= 0.8   && < 0.9