clean up and combine I/O and parsing under Hledger.Read.*
This facilitates adding readers for new data formats. Timelog parsing is temporarily broken.
This commit is contained in:
parent
f168124501
commit
a848a835a2
@ -8,6 +8,7 @@ A history-aware add command to help with data entry.
|
|||||||
module Hledger.Cli.Commands.Add
|
module Hledger.Cli.Commands.Add
|
||||||
where
|
where
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
|
import Hledger.Read.Journal (someamount)
|
||||||
import Hledger.Cli.Options
|
import Hledger.Cli.Options
|
||||||
import Hledger.Cli.Commands.Register (showRegisterReport)
|
import Hledger.Cli.Commands.Register (showRegisterReport)
|
||||||
#if __GLASGOW_HASKELL__ <= 610
|
#if __GLASGOW_HASKELL__ <= 610
|
||||||
@ -19,7 +20,7 @@ import System.IO ( stderr, hFlush, hPutStrLn, hPutStr )
|
|||||||
#endif
|
#endif
|
||||||
import System.IO.Error
|
import System.IO.Error
|
||||||
import Text.ParserCombinators.Parsec
|
import Text.ParserCombinators.Parsec
|
||||||
import Hledger.Cli.Utils (journalFromStringWithOpts)
|
import Hledger.Cli.Utils (readJournalWithOpts)
|
||||||
import qualified Data.Foldable as Foldable (find)
|
import qualified Data.Foldable as Foldable (find)
|
||||||
|
|
||||||
-- | Read ledger transactions from the terminal, prompting for each field,
|
-- | Read ledger transactions from the terminal, prompting for each field,
|
||||||
@ -157,7 +158,7 @@ appendToJournalFile Journal{filepath=f, jtext=t} s =
|
|||||||
registerFromString :: String -> IO String
|
registerFromString :: String -> IO String
|
||||||
registerFromString s = do
|
registerFromString s = do
|
||||||
now <- getCurrentLocalTime
|
now <- getCurrentLocalTime
|
||||||
l <- journalFromStringWithOpts [] s
|
l <- readJournalWithOpts [] s
|
||||||
return $ showRegisterReport opts (optsToFilterSpec opts [] now) l
|
return $ showRegisterReport opts (optsToFilterSpec opts [] now) l
|
||||||
where opts = [Empty]
|
where opts = [Empty]
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import Hledger.Cli.Options (Opt(Debug))
|
|||||||
import Hledger.Cli.Version (versionstr)
|
import Hledger.Cli.Version (versionstr)
|
||||||
import Hledger.Data.Types (Journal,AccountName,Transaction(..),Posting(..),PostingType(..))
|
import Hledger.Data.Types (Journal,AccountName,Transaction(..),Posting(..),PostingType(..))
|
||||||
import Hledger.Data.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual)
|
import Hledger.Data.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual)
|
||||||
import Hledger.Data.Parse (someamount, emptyCtx, ledgeraccountname)
|
import Hledger.Read.Common (emptyCtx)
|
||||||
|
import Hledger.Read.Journal (someamount,ledgeraccountname)
|
||||||
import Hledger.Data.Amount (nullmixedamt)
|
import Hledger.Data.Amount (nullmixedamt)
|
||||||
import Safe (atDef, maximumDef)
|
import Safe (atDef, maximumDef)
|
||||||
import System.IO (stderr)
|
import System.IO (stderr)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import Hledger.Cli.Options
|
|||||||
import Prelude hiding ( putStr )
|
import Prelude hiding ( putStr )
|
||||||
import System.IO.UTF8
|
import System.IO.UTF8
|
||||||
#endif
|
#endif
|
||||||
|
import Text.ParserCombinators.Parsec
|
||||||
|
|
||||||
|
|
||||||
-- | Print a register report.
|
-- | Print a register report.
|
||||||
@ -71,6 +72,28 @@ displayExprMatches :: Maybe String -> Posting -> Bool
|
|||||||
displayExprMatches Nothing _ = True
|
displayExprMatches Nothing _ = True
|
||||||
displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p
|
displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p
|
||||||
|
|
||||||
|
-- | Parse a hledger display expression, which is a simple date test like
|
||||||
|
-- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.
|
||||||
|
datedisplayexpr :: GenParser Char st (Posting -> Bool)
|
||||||
|
datedisplayexpr = do
|
||||||
|
char 'd'
|
||||||
|
op <- compareop
|
||||||
|
char '['
|
||||||
|
(y,m,d) <- smartdate
|
||||||
|
char ']'
|
||||||
|
let date = parsedate $ printf "%04s/%02s/%02s" y m d
|
||||||
|
test op = return $ (`op` date) . postingDate
|
||||||
|
case op of
|
||||||
|
"<" -> test (<)
|
||||||
|
"<=" -> test (<=)
|
||||||
|
"=" -> test (==)
|
||||||
|
"==" -> test (==)
|
||||||
|
">=" -> test (>=)
|
||||||
|
">" -> test (>)
|
||||||
|
_ -> mzero
|
||||||
|
where
|
||||||
|
compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]
|
||||||
|
|
||||||
-- XXX confusing, refactor
|
-- XXX confusing, refactor
|
||||||
-- | Given a date span (representing a reporting interval) and a list of
|
-- | Given a date span (representing a reporting interval) and a list of
|
||||||
-- postings within it: aggregate the postings so there is only one per
|
-- postings within it: aggregate the postings so there is only one per
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import Hledger.Cli.Commands.Histogram
|
|||||||
import Hledger.Cli.Commands.Print
|
import Hledger.Cli.Commands.Print
|
||||||
import Hledger.Cli.Commands.Register
|
import Hledger.Cli.Commands.Register
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
|
import Hledger.Read
|
||||||
import Hledger.Cli.Options hiding (value)
|
import Hledger.Cli.Options hiding (value)
|
||||||
#ifdef MAKE
|
#ifdef MAKE
|
||||||
import Paths_hledger_make (getDataFileName)
|
import Paths_hledger_make (getDataFileName)
|
||||||
@ -114,7 +115,7 @@ journalFileModifiedTime Journal{filepath=f}
|
|||||||
|
|
||||||
reload :: Journal -> IO Journal
|
reload :: Journal -> IO Journal
|
||||||
reload Journal{filepath=f} = do
|
reload Journal{filepath=f} = do
|
||||||
j' <- readJournal f
|
j' <- readJournalFile f
|
||||||
putValue "hledger" "journal" j'
|
putValue "hledger" "journal" j'
|
||||||
return j'
|
return j'
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ You can use the command line:
|
|||||||
or ghci:
|
or ghci:
|
||||||
|
|
||||||
> $ ghci hledger
|
> $ ghci hledger
|
||||||
> > j <- readJournal "data/sample.journal"
|
> > j <- readJournalFile "data/sample.journal"
|
||||||
> > register [] ["income","expenses"] j
|
> > register [] ["income","expenses"] j
|
||||||
> 2008/01/01 income income:salary $-1 $-1
|
> 2008/01/01 income income:salary $-1 $-1
|
||||||
> 2008/06/01 gift income:gifts $-1 $-2
|
> 2008/06/01 gift income:gifts $-1 $-2
|
||||||
|
|||||||
@ -8,7 +8,7 @@ where
|
|||||||
import System.Console.GetOpt
|
import System.Console.GetOpt
|
||||||
import System.Environment
|
import System.Environment
|
||||||
import Hledger.Cli.Version (timeprogname)
|
import Hledger.Cli.Version (timeprogname)
|
||||||
import Hledger.Data.IO (myLedgerPath,myTimelogPath)
|
import Hledger.Read (myLedgerPath,myTimelogPath)
|
||||||
import Hledger.Data.Utils
|
import Hledger.Data.Utils
|
||||||
import Hledger.Data.Types
|
import Hledger.Data.Types
|
||||||
import Hledger.Data.Dates
|
import Hledger.Data.Dates
|
||||||
|
|||||||
@ -35,6 +35,8 @@ import System.Time (ClockTime(TOD))
|
|||||||
|
|
||||||
import Hledger.Cli.Commands.All
|
import Hledger.Cli.Commands.All
|
||||||
import Hledger.Data -- including testing utils in Hledger.Data.Utils
|
import Hledger.Data -- including testing utils in Hledger.Data.Utils
|
||||||
|
import Hledger.Read.Common (emptyCtx)
|
||||||
|
import Hledger.Read (someamount,readJournal)
|
||||||
import Hledger.Cli.Options
|
import Hledger.Cli.Options
|
||||||
import Hledger.Cli.Utils
|
import Hledger.Cli.Utils
|
||||||
|
|
||||||
@ -61,8 +63,8 @@ tests = TestList [
|
|||||||
tests_Hledger_Commands,
|
tests_Hledger_Commands,
|
||||||
|
|
||||||
"account directive" ~:
|
"account directive" ~:
|
||||||
let sameParse str1 str2 = do j1 <- journalFromString str1
|
let sameParse str1 str2 = do j1 <- readJournal str1
|
||||||
j2 <- journalFromString str2
|
j2 <- readJournal str2
|
||||||
j1 `is` j2{filereadtime=filereadtime j1, jtext=jtext j1}
|
j1 `is` j2{filereadtime=filereadtime j1, jtext=jtext j1}
|
||||||
in TestList
|
in TestList
|
||||||
[
|
[
|
||||||
@ -229,7 +231,7 @@ tests = TestList [
|
|||||||
]
|
]
|
||||||
|
|
||||||
,"balance report with cost basis" ~: do
|
,"balance report with cost basis" ~: do
|
||||||
j <- journalFromString $ unlines
|
j <- readJournal $ unlines
|
||||||
[""
|
[""
|
||||||
,"2008/1/1 test "
|
,"2008/1/1 test "
|
||||||
," a:b 10h @ $50"
|
," a:b 10h @ $50"
|
||||||
@ -244,7 +246,7 @@ tests = TestList [
|
|||||||
]
|
]
|
||||||
|
|
||||||
,"balance report elides zero-balance root account(s)" ~: do
|
,"balance report elides zero-balance root account(s)" ~: do
|
||||||
l <- journalFromStringWithOpts []
|
l <- readJournalWithOpts []
|
||||||
(unlines
|
(unlines
|
||||||
["2008/1/1 one"
|
["2008/1/1 one"
|
||||||
," test:a 1"
|
," test:a 1"
|
||||||
@ -372,15 +374,10 @@ tests = TestList [
|
|||||||
"assets:bank" `isSubAccountNameOf` "my assets" `is` False
|
"assets:bank" `isSubAccountNameOf` "my assets" `is` False
|
||||||
|
|
||||||
,"default year" ~: do
|
,"default year" ~: do
|
||||||
rl <- journalFromString defaultyear_ledger_str
|
rl <- readJournal defaultyear_ledger_str
|
||||||
tdate (head $ jtxns rl) `is` fromGregorian 2009 1 1
|
tdate (head $ jtxns rl) `is` fromGregorian 2009 1 1
|
||||||
return ()
|
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" ~: do
|
||||||
normaliseMixedAmount (Mixed []) ~?= Mixed [nullamt]
|
normaliseMixedAmount (Mixed []) ~?= Mixed [nullamt]
|
||||||
|
|
||||||
@ -468,7 +465,7 @@ tests = TestList [
|
|||||||
,"register report with cleared option" ~:
|
,"register report with cleared option" ~:
|
||||||
do
|
do
|
||||||
let opts = [Cleared]
|
let opts = [Cleared]
|
||||||
l <- journalFromStringWithOpts opts sample_ledger_str
|
l <- readJournalWithOpts opts sample_ledger_str
|
||||||
showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines
|
showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines
|
||||||
["2008/06/03 eat & shop expenses:food $1 $1"
|
["2008/06/03 eat & shop expenses:food $1 $1"
|
||||||
," expenses:supplies $1 $2"
|
," expenses:supplies $1 $2"
|
||||||
@ -480,7 +477,7 @@ tests = TestList [
|
|||||||
,"register report with uncleared option" ~:
|
,"register report with uncleared option" ~:
|
||||||
do
|
do
|
||||||
let opts = [UnCleared]
|
let opts = [UnCleared]
|
||||||
l <- journalFromStringWithOpts opts sample_ledger_str
|
l <- readJournalWithOpts opts sample_ledger_str
|
||||||
showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines
|
showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines
|
||||||
["2008/01/01 income assets:bank:checking $1 $1"
|
["2008/01/01 income assets:bank:checking $1 $1"
|
||||||
," income:salary $-1 0"
|
," income:salary $-1 0"
|
||||||
@ -492,7 +489,7 @@ tests = TestList [
|
|||||||
|
|
||||||
,"register report sorts by date" ~:
|
,"register report sorts by date" ~:
|
||||||
do
|
do
|
||||||
l <- journalFromStringWithOpts [] $ unlines
|
l <- readJournalWithOpts [] $ unlines
|
||||||
["2008/02/02 a"
|
["2008/02/02 a"
|
||||||
," b 1"
|
," b 1"
|
||||||
," c"
|
," c"
|
||||||
@ -577,14 +574,14 @@ tests = TestList [
|
|||||||
,"show hours" ~: show (hours 1) ~?= "1.0h"
|
,"show hours" ~: show (hours 1) ~?= "1.0h"
|
||||||
|
|
||||||
,"unicode in balance layout" ~: do
|
,"unicode in balance layout" ~: do
|
||||||
l <- journalFromStringWithOpts []
|
l <- readJournalWithOpts []
|
||||||
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
||||||
showBalanceReport [] (optsToFilterSpec [] [] t1) l `is` unlines
|
showBalanceReport [] (optsToFilterSpec [] [] t1) l `is` unlines
|
||||||
[" -100 актив:наличные"
|
[" -100 актив:наличные"
|
||||||
," 100 расходы:покупки"]
|
," 100 расходы:покупки"]
|
||||||
|
|
||||||
,"unicode in register layout" ~: do
|
,"unicode in register layout" ~: do
|
||||||
l <- journalFromStringWithOpts []
|
l <- readJournalWithOpts []
|
||||||
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
|
||||||
showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines
|
showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines
|
||||||
["2009/01/01 медвежья шкура расходы:покупки 100 100"
|
["2009/01/01 медвежья шкура расходы:покупки 100 100"
|
||||||
@ -673,8 +670,8 @@ tests = TestList [
|
|||||||
date1 = parsedate "2008/11/26"
|
date1 = parsedate "2008/11/26"
|
||||||
t1 = LocalTime date1 midday
|
t1 = LocalTime date1 midday
|
||||||
|
|
||||||
sampleledger = journalFromStringWithOpts [] sample_ledger_str
|
sampleledger = readJournalWithOpts [] sample_ledger_str
|
||||||
sampleledgerwithopts opts _ = journalFromStringWithOpts opts sample_ledger_str
|
sampleledgerwithopts opts _ = readJournalWithOpts opts sample_ledger_str
|
||||||
|
|
||||||
sample_ledger_str = unlines
|
sample_ledger_str = unlines
|
||||||
["; A sample ledger file."
|
["; A sample ledger file."
|
||||||
@ -1078,5 +1075,5 @@ journalWithAmounts as =
|
|||||||
""
|
""
|
||||||
(TOD 0 0)
|
(TOD 0 0)
|
||||||
""
|
""
|
||||||
where parse = fromparse . parseWithCtx emptyCtx postingamount . (" "++)
|
where parse = fromparse . parseWithCtx emptyCtx someamount
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
Utilities for top-level modules and ghci. See also Hledger.Data.IO and
|
Utilities for top-level modules and ghci. See also Hledger.Read and
|
||||||
Hledger.Data.Utils.
|
Hledger.Data.Utils.
|
||||||
|
|
||||||
-}
|
-}
|
||||||
@ -9,27 +9,21 @@ Hledger.Data.Utils.
|
|||||||
module Hledger.Cli.Utils
|
module Hledger.Cli.Utils
|
||||||
(
|
(
|
||||||
withJournalDo,
|
withJournalDo,
|
||||||
journalFromStringWithOpts,
|
readJournalWithOpts,
|
||||||
openBrowserOn
|
openBrowserOn
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
import Control.Monad.Error
|
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
|
import Hledger.Read
|
||||||
import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec)
|
import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec)
|
||||||
import System.Directory (doesFileExist)
|
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.Exit
|
||||||
import System.Process (readProcessWithExitCode)
|
|
||||||
import System.Info (os)
|
import System.Info (os)
|
||||||
|
import System.Process (readProcessWithExitCode)
|
||||||
|
|
||||||
|
|
||||||
-- | Parse the user's specified journal file and run a hledger command on
|
-- | 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.
|
-- it, or throw an error.
|
||||||
withJournalDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Journal -> IO ()) -> IO ()
|
withJournalDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Journal -> IO ()) -> IO ()
|
||||||
withJournalDo opts args cmdname cmd = do
|
withJournalDo opts args cmdname cmd = do
|
||||||
-- We kludgily read the file before parsing to grab the full text, unless
|
-- We kludgily read the file before parsing to grab the full text, unless
|
||||||
@ -42,13 +36,12 @@ withJournalDo opts args cmdname cmd = do
|
|||||||
runcmd = cmd opts args . costify
|
runcmd = cmd opts args . costify
|
||||||
if creating
|
if creating
|
||||||
then runcmd nulljournal
|
then runcmd nulljournal
|
||||||
else (runErrorT . parseJournalFile) f >>= either parseerror runcmd
|
else readJournalFile f >>= runcmd
|
||||||
where parseerror e = hPutStrLn stderr e >> exitWith (ExitFailure 1)
|
|
||||||
|
|
||||||
-- | Get a journal from the given string and options, or throw an error.
|
-- | Get a journal from the given string and options, or throw an error.
|
||||||
journalFromStringWithOpts :: [Opt] -> String -> IO Journal
|
readJournalWithOpts :: [Opt] -> String -> IO Journal
|
||||||
journalFromStringWithOpts opts s = do
|
readJournalWithOpts opts s = do
|
||||||
j <- journalFromString s
|
j <- readJournal s
|
||||||
let cost = CostBasis `elem` opts
|
let cost = CostBasis `elem` opts
|
||||||
return $ (if cost then journalConvertAmountsToCost else id) j
|
return $ (if cost then journalConvertAmountsToCost else id) j
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,8 @@ module Hledger.Data (
|
|||||||
module Hledger.Data.Amount,
|
module Hledger.Data.Amount,
|
||||||
module Hledger.Data.Commodity,
|
module Hledger.Data.Commodity,
|
||||||
module Hledger.Data.Dates,
|
module Hledger.Data.Dates,
|
||||||
module Hledger.Data.IO,
|
|
||||||
module Hledger.Data.Transaction,
|
module Hledger.Data.Transaction,
|
||||||
module Hledger.Data.Ledger,
|
module Hledger.Data.Ledger,
|
||||||
module Hledger.Data.Parse,
|
|
||||||
module Hledger.Data.Journal,
|
module Hledger.Data.Journal,
|
||||||
module Hledger.Data.Posting,
|
module Hledger.Data.Posting,
|
||||||
module Hledger.Data.TimeLog,
|
module Hledger.Data.TimeLog,
|
||||||
@ -29,10 +27,8 @@ import Hledger.Data.AccountName
|
|||||||
import Hledger.Data.Amount
|
import Hledger.Data.Amount
|
||||||
import Hledger.Data.Commodity
|
import Hledger.Data.Commodity
|
||||||
import Hledger.Data.Dates
|
import Hledger.Data.Dates
|
||||||
import Hledger.Data.IO
|
|
||||||
import Hledger.Data.Transaction
|
import Hledger.Data.Transaction
|
||||||
import Hledger.Data.Ledger
|
import Hledger.Data.Ledger
|
||||||
import Hledger.Data.Parse
|
|
||||||
import Hledger.Data.Journal
|
import Hledger.Data.Journal
|
||||||
import Hledger.Data.Posting
|
import Hledger.Data.Posting
|
||||||
import Hledger.Data.TimeLog
|
import Hledger.Data.TimeLog
|
||||||
@ -46,10 +42,8 @@ tests_Hledger_Data = TestList
|
|||||||
Hledger.Data.Amount.tests_Amount
|
Hledger.Data.Amount.tests_Amount
|
||||||
-- ,Hledger.Data.Commodity.tests_Commodity
|
-- ,Hledger.Data.Commodity.tests_Commodity
|
||||||
,Hledger.Data.Dates.tests_Dates
|
,Hledger.Data.Dates.tests_Dates
|
||||||
-- ,Hledger.Data.IO.tests_IO
|
|
||||||
,Hledger.Data.Transaction.tests_Transaction
|
,Hledger.Data.Transaction.tests_Transaction
|
||||||
-- ,Hledger.Data.Hledger.Data.tests_Hledger.Data
|
-- ,Hledger.Data.Hledger.Data.tests_Hledger.Data
|
||||||
,Hledger.Data.Parse.tests_Parse
|
|
||||||
-- ,Hledger.Data.Journal.tests_Journal
|
-- ,Hledger.Data.Journal.tests_Journal
|
||||||
-- ,Hledger.Data.Posting.tests_Posting
|
-- ,Hledger.Data.Posting.tests_Posting
|
||||||
,Hledger.Data.TimeLog.tests_TimeLog
|
,Hledger.Data.TimeLog.tests_TimeLog
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import Hledger.Data.Utils
|
|||||||
import Hledger.Data.Types
|
import Hledger.Data.Types
|
||||||
import Hledger.Data.AccountName
|
import Hledger.Data.AccountName
|
||||||
import Hledger.Data.Amount
|
import Hledger.Data.Amount
|
||||||
|
import Hledger.Data.Dates (nulldatespan)
|
||||||
import Hledger.Data.Transaction (ledgerTransactionWithDate)
|
import Hledger.Data.Transaction (ledgerTransactionWithDate)
|
||||||
import Hledger.Data.Posting
|
import Hledger.Data.Posting
|
||||||
import Hledger.Data.TimeLog
|
import Hledger.Data.TimeLog
|
||||||
@ -43,6 +44,18 @@ nulljournal = Journal { jmodifiertxns = []
|
|||||||
, jtext = ""
|
, jtext = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nullfilterspec = FilterSpec {
|
||||||
|
datespan=nulldatespan
|
||||||
|
,cleared=Nothing
|
||||||
|
,real=False
|
||||||
|
,empty=False
|
||||||
|
,costbasis=False
|
||||||
|
,acctpats=[]
|
||||||
|
,descpats=[]
|
||||||
|
,whichdate=ActualDate
|
||||||
|
,depth=Nothing
|
||||||
|
}
|
||||||
|
|
||||||
addTransaction :: Transaction -> Journal -> Journal
|
addTransaction :: Transaction -> Journal -> Journal
|
||||||
addTransaction t l0 = l0 { jtxns = t : jtxns l0 }
|
addTransaction t l0 = l0 { jtxns = t : jtxns l0 }
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,39 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-|
|
{-|
|
||||||
Utilities for doing I/O with ledger files.
|
|
||||||
|
Read hledger data from various data formats, and related utilities.
|
||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
module Hledger.Data.IO
|
module Hledger.Read (
|
||||||
|
tests_Hledger_Read,
|
||||||
|
module Hledger.Read.Common,
|
||||||
|
Hledger.Read.Journal.someamount,
|
||||||
|
readJournalFile,
|
||||||
|
readJournal,
|
||||||
|
myLedgerPath,
|
||||||
|
myTimelogPath,
|
||||||
|
myJournal,
|
||||||
|
myTimelog,
|
||||||
|
)
|
||||||
where
|
where
|
||||||
|
import Hledger.Data.Types (Journal(..))
|
||||||
|
import Hledger.Data.Utils
|
||||||
|
import Hledger.Read.Common
|
||||||
|
import qualified Hledger.Read.Journal (parseJournal,parseJournalFile,ledgerFile,someamount,tests_Journal)
|
||||||
|
import qualified Hledger.Read.Timelog (tests_Timelog) --parseJournal
|
||||||
|
|
||||||
import Control.Monad.Error
|
import Control.Monad.Error
|
||||||
import Hledger.Data.Parse (parseJournal)
|
|
||||||
import Hledger.Data.Types (FilterSpec(..),WhichDate(..),Journal(..))
|
|
||||||
import Hledger.Data.Dates (nulldatespan)
|
|
||||||
import System.Directory (getHomeDirectory)
|
import System.Directory (getHomeDirectory)
|
||||||
import System.Environment (getEnv)
|
import System.Environment (getEnv)
|
||||||
#if __GLASGOW_HASKELL__ <= 610
|
|
||||||
import Prelude hiding (readFile)
|
|
||||||
import System.IO.UTF8
|
|
||||||
#endif
|
|
||||||
import System.FilePath ((</>))
|
import System.FilePath ((</>))
|
||||||
|
import System.Exit
|
||||||
|
import System.IO (stderr)
|
||||||
|
#if __GLASGOW_HASKELL__ <= 610
|
||||||
|
import System.IO.UTF8 (hPutStrLn)
|
||||||
|
#else
|
||||||
|
import System.IO (hPutStrLn)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
ledgerenvvar = "LEDGER"
|
ledgerenvvar = "LEDGER"
|
||||||
@ -23,18 +41,6 @@ timelogenvvar = "TIMELOG"
|
|||||||
ledgerdefaultfilename = ".ledger"
|
ledgerdefaultfilename = ".ledger"
|
||||||
timelogdefaultfilename = ".timelog"
|
timelogdefaultfilename = ".timelog"
|
||||||
|
|
||||||
nullfilterspec = FilterSpec {
|
|
||||||
datespan=nulldatespan
|
|
||||||
,cleared=Nothing
|
|
||||||
,real=False
|
|
||||||
,empty=False
|
|
||||||
,costbasis=False
|
|
||||||
,acctpats=[]
|
|
||||||
,descpats=[]
|
|
||||||
,whichdate=ActualDate
|
|
||||||
,depth=Nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Get the user's default ledger file path.
|
-- | Get the user's default ledger file path.
|
||||||
myLedgerPath :: IO String
|
myLedgerPath :: IO String
|
||||||
myLedgerPath =
|
myLedgerPath =
|
||||||
@ -53,23 +59,22 @@ myTimelogPath =
|
|||||||
|
|
||||||
-- | Read the user's default journal file, or give an error.
|
-- | Read the user's default journal file, or give an error.
|
||||||
myJournal :: IO Journal
|
myJournal :: IO Journal
|
||||||
myJournal = myLedgerPath >>= readJournal
|
myJournal = myLedgerPath >>= readJournalFile
|
||||||
|
|
||||||
-- | Read the user's default timelog file, or give an error.
|
-- | Read the user's default timelog file, or give an error.
|
||||||
myTimelog :: IO Journal
|
myTimelog :: IO Journal
|
||||||
myTimelog = myTimelogPath >>= readJournal
|
myTimelog = myTimelogPath >>= readJournalFile
|
||||||
|
|
||||||
-- | Read a journal from this file, or throw an error.
|
-- | Read a journal from this file, or throw an error.
|
||||||
readJournal :: FilePath -> IO Journal
|
readJournalFile :: FilePath -> IO Journal
|
||||||
readJournal f = do
|
readJournalFile f =
|
||||||
s <- readFile f
|
(runErrorT . Hledger.Read.Journal.parseJournalFile) f >>= either printerror return
|
||||||
j <- journalFromString s
|
where printerror e = hPutStrLn stderr e >> exitWith (ExitFailure 1)
|
||||||
return j{filepath=f}
|
|
||||||
|
|
||||||
-- | Read a Journal from the given string, using the current time as
|
-- | Read a Journal from this string, or throw an error.
|
||||||
-- reference time, or throw an error.
|
readJournal :: String -> IO Journal
|
||||||
journalFromString :: String -> IO Journal
|
readJournal s =
|
||||||
journalFromString s = liftM (either error id) $ runErrorT $ parseJournal "(from string)" s
|
(runErrorT . Hledger.Read.Journal.parseJournal "(from string)") s >>= either error return
|
||||||
|
|
||||||
-- -- | Expand ~ in a file path (does not handle ~name).
|
-- -- | Expand ~ in a file path (does not handle ~name).
|
||||||
-- tildeExpand :: FilePath -> IO FilePath
|
-- tildeExpand :: FilePath -> IO FilePath
|
||||||
@ -82,3 +87,14 @@ journalFromString s = liftM (either error id) $ runErrorT $ parseJournal "(from
|
|||||||
-- -- return (homeDirectory pw ++ path)
|
-- -- return (homeDirectory pw ++ path)
|
||||||
-- tildeExpand xs = return xs
|
-- tildeExpand xs = return xs
|
||||||
|
|
||||||
|
tests_Hledger_Read = TestList
|
||||||
|
[
|
||||||
|
|
||||||
|
"ledgerFile" ~: do
|
||||||
|
assertBool "ledgerFile should parse an empty file" (isRight $ parseWithCtx emptyCtx Hledger.Read.Journal.ledgerFile "")
|
||||||
|
r <- readJournal "" -- don't know how to get it from ledgerFile
|
||||||
|
assertBool "ledgerFile parsing an empty file should give an empty ledger" $ null $ jtxns r
|
||||||
|
|
||||||
|
,Hledger.Read.Journal.tests_Journal
|
||||||
|
,Hledger.Read.Timelog.tests_Timelog
|
||||||
|
]
|
||||||
60
hledger-lib/Hledger/Read/Common.hs
Normal file
60
hledger-lib/Hledger/Read/Common.hs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{-|
|
||||||
|
|
||||||
|
Common utilities for hledger data readers, such as the context (state)
|
||||||
|
that is kept while parsing a journal.
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Hledger.Read.Common
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Monad.Error
|
||||||
|
import Data.List
|
||||||
|
import Hledger.Data.Types (Journal)
|
||||||
|
import Text.ParserCombinators.Parsec
|
||||||
|
import System.Directory (getHomeDirectory)
|
||||||
|
import System.FilePath(takeDirectory,combine)
|
||||||
|
|
||||||
|
|
||||||
|
-- | A JournalUpdate is some transformation of a "Journal". It can do I/O
|
||||||
|
-- or raise an error.
|
||||||
|
type JournalUpdate = ErrorT String IO (Journal -> Journal)
|
||||||
|
|
||||||
|
-- | Some context kept during parsing.
|
||||||
|
data LedgerFileCtx = Ctx {
|
||||||
|
ctxYear :: !(Maybe Integer) -- ^ the default year most recently specified with Y
|
||||||
|
, ctxCommod :: !(Maybe String) -- ^ I don't know
|
||||||
|
, ctxAccount :: ![String] -- ^ the current stack of parent accounts specified by !account
|
||||||
|
} deriving (Read, Show)
|
||||||
|
|
||||||
|
emptyCtx :: LedgerFileCtx
|
||||||
|
emptyCtx = Ctx { ctxYear = Nothing, ctxCommod = Nothing, ctxAccount = [] }
|
||||||
|
|
||||||
|
setYear :: Integer -> GenParser tok LedgerFileCtx ()
|
||||||
|
setYear y = updateState (\ctx -> ctx{ctxYear=Just y})
|
||||||
|
|
||||||
|
getYear :: GenParser tok LedgerFileCtx (Maybe Integer)
|
||||||
|
getYear = liftM ctxYear getState
|
||||||
|
|
||||||
|
pushParentAccount :: String -> GenParser tok LedgerFileCtx ()
|
||||||
|
pushParentAccount parent = updateState addParentAccount
|
||||||
|
where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 }
|
||||||
|
normalize = (++ ":")
|
||||||
|
|
||||||
|
popParentAccount :: GenParser tok LedgerFileCtx ()
|
||||||
|
popParentAccount = do ctx0 <- getState
|
||||||
|
case ctxAccount ctx0 of
|
||||||
|
[] -> unexpected "End of account block with no beginning"
|
||||||
|
(_:rest) -> setState $ ctx0 { ctxAccount = rest }
|
||||||
|
|
||||||
|
getParentAccount :: GenParser tok LedgerFileCtx String
|
||||||
|
getParentAccount = liftM (concat . reverse . ctxAccount) getState
|
||||||
|
|
||||||
|
expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath
|
||||||
|
expandPath pos fp = liftM mkRelative (expandHome fp)
|
||||||
|
where
|
||||||
|
mkRelative = combine (takeDirectory (sourceName pos))
|
||||||
|
expandHome inname | "~/" `isPrefixOf` inname = do homedir <- liftIO getHomeDirectory
|
||||||
|
return $ homedir ++ drop 1 inname
|
||||||
|
| otherwise = return inname
|
||||||
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
Parsers for hledger's journal file format and the timelog file format.
|
A reader for hledger's (and c++ ledger's) journal file format.
|
||||||
|
|
||||||
Here is the ledger grammar from the ledger 2.5 manual:
|
From the ledger 2.5 manual:
|
||||||
|
|
||||||
@
|
@
|
||||||
The ledger file format is quite simple, but also very flexible. It supports
|
The ledger file format is quite simple, but also very flexible. It supports
|
||||||
@ -101,50 +101,18 @@ i, o, b, h
|
|||||||
timelog files.
|
timelog files.
|
||||||
@
|
@
|
||||||
|
|
||||||
Here is the timelog grammar from timeclock.el 2.6:
|
|
||||||
|
|
||||||
@
|
|
||||||
A timelog contains data in the form of a single entry per line.
|
|
||||||
Each entry has the form:
|
|
||||||
|
|
||||||
CODE YYYY/MM/DD HH:MM:SS [COMMENT]
|
|
||||||
|
|
||||||
CODE is one of: b, h, i, o or O. COMMENT is optional when the code is
|
|
||||||
i, o or O. The meanings of the codes are:
|
|
||||||
|
|
||||||
b Set the current time balance, or \"time debt\". Useful when
|
|
||||||
archiving old log data, when a debt must be carried forward.
|
|
||||||
The COMMENT here is the number of seconds of debt.
|
|
||||||
|
|
||||||
h Set the required working time for the given day. This must
|
|
||||||
be the first entry for that day. The COMMENT in this case is
|
|
||||||
the number of hours in this workday. Floating point amounts
|
|
||||||
are allowed.
|
|
||||||
|
|
||||||
i Clock in. The COMMENT in this case should be the name of the
|
|
||||||
project worked on.
|
|
||||||
|
|
||||||
o Clock out. COMMENT is unnecessary, but can be used to provide
|
|
||||||
a description of how the period went, for example.
|
|
||||||
|
|
||||||
O Final clock out. Whatever project was being worked on, it is
|
|
||||||
now finished. Useful for creating summary reports.
|
|
||||||
@
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
@
|
|
||||||
i 2007/03/10 12:26:00 hledger
|
|
||||||
o 2007/03/10 17:26:02
|
|
||||||
@
|
|
||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
module Hledger.Data.Parse
|
module Hledger.Read.Journal {- (
|
||||||
|
parseJournal,
|
||||||
|
parseJournalFile,
|
||||||
|
someamount,
|
||||||
|
emptyCtx,
|
||||||
|
ledgeraccountname
|
||||||
|
) -}
|
||||||
where
|
where
|
||||||
import Control.Monad.Error (ErrorT(..), MonadIO, liftIO, throwError, catchError)
|
import Control.Monad.Error (ErrorT(..), MonadIO, liftIO, throwError, catchError)
|
||||||
import Text.ParserCombinators.Parsec
|
import Text.ParserCombinators.Parsec
|
||||||
import System.Directory
|
|
||||||
#if __GLASGOW_HASKELL__ <= 610
|
#if __GLASGOW_HASKELL__ <= 610
|
||||||
import Prelude hiding (readFile, putStr, putStrLn, print, getContents)
|
import Prelude hiding (readFile, putStr, putStrLn, print, getContents)
|
||||||
import System.IO.UTF8
|
import System.IO.UTF8
|
||||||
@ -158,52 +126,10 @@ import Hledger.Data.Transaction
|
|||||||
import Hledger.Data.Posting
|
import Hledger.Data.Posting
|
||||||
import Hledger.Data.Journal
|
import Hledger.Data.Journal
|
||||||
import Hledger.Data.Commodity (dollars,dollar,unknown,nonsimplecommoditychars)
|
import Hledger.Data.Commodity (dollars,dollar,unknown,nonsimplecommoditychars)
|
||||||
import System.FilePath(takeDirectory,combine)
|
import Hledger.Read.Common
|
||||||
import System.Time (getClockTime)
|
import System.Time (getClockTime)
|
||||||
|
|
||||||
|
|
||||||
-- | A JournalUpdate is some transformation of a "Journal". It can do I/O
|
|
||||||
-- or raise an error.
|
|
||||||
type JournalUpdate = ErrorT String IO (Journal -> Journal)
|
|
||||||
|
|
||||||
-- | Some context kept during parsing.
|
|
||||||
data LedgerFileCtx = Ctx {
|
|
||||||
ctxYear :: !(Maybe Integer) -- ^ the default year most recently specified with Y
|
|
||||||
, ctxCommod :: !(Maybe String) -- ^ I don't know
|
|
||||||
, ctxAccount :: ![String] -- ^ the current stack of parent accounts specified by !account
|
|
||||||
} deriving (Read, Show)
|
|
||||||
|
|
||||||
emptyCtx :: LedgerFileCtx
|
|
||||||
emptyCtx = Ctx { ctxYear = Nothing, ctxCommod = Nothing, ctxAccount = [] }
|
|
||||||
|
|
||||||
setYear :: Integer -> GenParser tok LedgerFileCtx ()
|
|
||||||
setYear y = updateState (\ctx -> ctx{ctxYear=Just y})
|
|
||||||
|
|
||||||
getYear :: GenParser tok LedgerFileCtx (Maybe Integer)
|
|
||||||
getYear = liftM ctxYear getState
|
|
||||||
|
|
||||||
pushParentAccount :: String -> GenParser tok LedgerFileCtx ()
|
|
||||||
pushParentAccount parent = updateState addParentAccount
|
|
||||||
where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 }
|
|
||||||
normalize = (++ ":")
|
|
||||||
|
|
||||||
popParentAccount :: GenParser tok LedgerFileCtx ()
|
|
||||||
popParentAccount = do ctx0 <- getState
|
|
||||||
case ctxAccount ctx0 of
|
|
||||||
[] -> unexpected "End of account block with no beginning"
|
|
||||||
(_:rest) -> setState $ ctx0 { ctxAccount = rest }
|
|
||||||
|
|
||||||
getParentAccount :: GenParser tok LedgerFileCtx String
|
|
||||||
getParentAccount = liftM (concat . reverse . ctxAccount) getState
|
|
||||||
|
|
||||||
expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath
|
|
||||||
expandPath pos fp = liftM mkRelative (expandHome fp)
|
|
||||||
where
|
|
||||||
mkRelative = combine (takeDirectory (sourceName pos))
|
|
||||||
expandHome inname | "~/" `isPrefixOf` inname = do homedir <- liftIO getHomeDirectory
|
|
||||||
return $ homedir ++ drop 1 inname
|
|
||||||
| otherwise = return inname
|
|
||||||
|
|
||||||
-- let's get to it
|
-- let's get to it
|
||||||
|
|
||||||
-- | Parse and post-process a journal file or timelog file to a "Journal",
|
-- | Parse and post-process a journal file or timelog file to a "Journal",
|
||||||
@ -212,8 +138,9 @@ parseJournalFile :: FilePath -> ErrorT String IO Journal
|
|||||||
parseJournalFile "-" = liftIO getContents >>= parseJournal "-"
|
parseJournalFile "-" = liftIO getContents >>= parseJournal "-"
|
||||||
parseJournalFile f = liftIO (readFile f) >>= parseJournal f
|
parseJournalFile f = liftIO (readFile f) >>= parseJournal f
|
||||||
|
|
||||||
-- | Parse and post-process a "Journal" from a string, saving the provided
|
-- | Parse and post-process a "Journal" from hledger's journal file
|
||||||
-- file path and the current time, or give an error.
|
-- format, saving the provided file path and the current time, or give an
|
||||||
|
-- error.
|
||||||
parseJournal :: FilePath -> String -> ErrorT String IO Journal
|
parseJournal :: FilePath -> String -> ErrorT String IO Journal
|
||||||
parseJournal f s = do
|
parseJournal f s = do
|
||||||
tc <- liftIO getClockTime
|
tc <- liftIO getClockTime
|
||||||
@ -243,7 +170,6 @@ ledgerFile = do items <- many ledgerItem
|
|||||||
, ledgerTagDirective
|
, ledgerTagDirective
|
||||||
, ledgerEndTagDirective
|
, ledgerEndTagDirective
|
||||||
, emptyLine >> return (return id)
|
, emptyLine >> return (return id)
|
||||||
, liftM (return . addTimeLogEntry) timelogentry
|
|
||||||
] <?> "ledger transaction, timelog entry, or directive"
|
] <?> "ledger transaction, timelog entry, or directive"
|
||||||
|
|
||||||
emptyLine :: GenParser Char st ()
|
emptyLine :: GenParser Char st ()
|
||||||
@ -610,40 +536,7 @@ numberpartsstartingwithpoint = do
|
|||||||
return ("",frac)
|
return ("",frac)
|
||||||
|
|
||||||
|
|
||||||
-- | Parse a timelog entry.
|
tests_Journal = TestList [
|
||||||
timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry
|
|
||||||
timelogentry = do
|
|
||||||
code <- oneOf "bhioO"
|
|
||||||
many1 spacenonewline
|
|
||||||
datetime <- ledgerdatetime
|
|
||||||
comment <- optionMaybe (many1 spacenonewline >> liftM2 (++) getParentAccount restofline)
|
|
||||||
return $ TimeLogEntry (read [code]) datetime (fromMaybe "" comment)
|
|
||||||
|
|
||||||
|
|
||||||
-- | Parse a hledger display expression, which is a simple date test like
|
|
||||||
-- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.
|
|
||||||
datedisplayexpr :: GenParser Char st (Posting -> Bool)
|
|
||||||
datedisplayexpr = do
|
|
||||||
char 'd'
|
|
||||||
op <- compareop
|
|
||||||
char '['
|
|
||||||
(y,m,d) <- smartdate
|
|
||||||
char ']'
|
|
||||||
let date = parsedate $ printf "%04s/%02s/%02s" y m d
|
|
||||||
test op = return $ (`op` date) . postingDate
|
|
||||||
case op of
|
|
||||||
"<" -> test (<)
|
|
||||||
"<=" -> test (<=)
|
|
||||||
"=" -> test (==)
|
|
||||||
"==" -> test (==)
|
|
||||||
">=" -> test (>=)
|
|
||||||
">" -> test (>)
|
|
||||||
_ -> mzero
|
|
||||||
|
|
||||||
compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]
|
|
||||||
|
|
||||||
|
|
||||||
tests_Parse = TestList [
|
|
||||||
|
|
||||||
"ledgerTransaction" ~: do
|
"ledgerTransaction" ~: do
|
||||||
assertParseEqual (parseWithCtx emptyCtx ledgerTransaction entry1_str) entry1
|
assertParseEqual (parseWithCtx emptyCtx ledgerTransaction entry1_str) entry1
|
||||||
@ -739,4 +632,3 @@ entry1 =
|
|||||||
[Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing,
|
[Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting Nothing,
|
||||||
Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting Nothing] ""
|
Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting Nothing] ""
|
||||||
|
|
||||||
|
|
||||||
95
hledger-lib/Hledger/Read/Timelog.hs
Normal file
95
hledger-lib/Hledger/Read/Timelog.hs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
{-# LANGUAGE CPP #-}
|
||||||
|
{-|
|
||||||
|
|
||||||
|
A reader for the timelog file format generated by timeclock.el.
|
||||||
|
|
||||||
|
From timeclock.el 2.6:
|
||||||
|
|
||||||
|
@
|
||||||
|
A timelog contains data in the form of a single entry per line.
|
||||||
|
Each entry has the form:
|
||||||
|
|
||||||
|
CODE YYYY/MM/DD HH:MM:SS [COMMENT]
|
||||||
|
|
||||||
|
CODE is one of: b, h, i, o or O. COMMENT is optional when the code is
|
||||||
|
i, o or O. The meanings of the codes are:
|
||||||
|
|
||||||
|
b Set the current time balance, or \"time debt\". Useful when
|
||||||
|
archiving old log data, when a debt must be carried forward.
|
||||||
|
The COMMENT here is the number of seconds of debt.
|
||||||
|
|
||||||
|
h Set the required working time for the given day. This must
|
||||||
|
be the first entry for that day. The COMMENT in this case is
|
||||||
|
the number of hours in this workday. Floating point amounts
|
||||||
|
are allowed.
|
||||||
|
|
||||||
|
i Clock in. The COMMENT in this case should be the name of the
|
||||||
|
project worked on.
|
||||||
|
|
||||||
|
o Clock out. COMMENT is unnecessary, but can be used to provide
|
||||||
|
a description of how the period went, for example.
|
||||||
|
|
||||||
|
O Final clock out. Whatever project was being worked on, it is
|
||||||
|
now finished. Useful for creating summary reports.
|
||||||
|
@
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
@
|
||||||
|
i 2007/03/10 12:26:00 hledger
|
||||||
|
o 2007/03/10 17:26:02
|
||||||
|
@
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Hledger.Read.Timelog {- (
|
||||||
|
parseJournal,
|
||||||
|
parseJournalFile
|
||||||
|
) -}
|
||||||
|
where
|
||||||
|
import Control.Monad.Error (ErrorT(..), MonadIO, liftIO, throwError)
|
||||||
|
import Text.ParserCombinators.Parsec
|
||||||
|
import System.Time (getClockTime)
|
||||||
|
import Hledger.Data
|
||||||
|
import Hledger.Read.Common (LedgerFileCtx,JournalUpdate,emptyCtx,getParentAccount)
|
||||||
|
import Hledger.Read.Journal hiding (parseJournal, parseJournalFile)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Parse and post-process a "Journal" from timeclock.el's timelog
|
||||||
|
-- format, saving the provided file path and the current time, or give an
|
||||||
|
-- error.
|
||||||
|
parseJournal :: FilePath -> String -> ErrorT String IO Journal
|
||||||
|
parseJournal f s = do
|
||||||
|
tc <- liftIO getClockTime
|
||||||
|
tl <- liftIO getCurrentLocalTime
|
||||||
|
case runParser timelogFile emptyCtx f s of
|
||||||
|
Right m -> liftM (journalFinalise tc tl f s) $ m `ap` return nulljournal
|
||||||
|
Left err -> throwError $ show err -- XXX raises an uncaught exception if we have a parsec user error, eg from many ?
|
||||||
|
|
||||||
|
timelogFile :: GenParser Char LedgerFileCtx JournalUpdate
|
||||||
|
timelogFile = do items <- many timelogItem
|
||||||
|
eof
|
||||||
|
return $ liftM (foldr (.) id) $ sequence items
|
||||||
|
where
|
||||||
|
-- As all ledger line types can be distinguished by the first
|
||||||
|
-- character, excepting transactions versus empty (blank or
|
||||||
|
-- comment-only) lines, can use choice w/o try
|
||||||
|
timelogItem = choice [ ledgerExclamationDirective
|
||||||
|
, liftM (return . addHistoricalPrice) ledgerHistoricalPrice
|
||||||
|
, ledgerDefaultYear
|
||||||
|
, emptyLine >> return (return id)
|
||||||
|
, liftM (return . addTimeLogEntry) timelogentry
|
||||||
|
] <?> "timelog entry, or default year or historical price directive"
|
||||||
|
|
||||||
|
-- | Parse a timelog entry.
|
||||||
|
timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry
|
||||||
|
timelogentry = do
|
||||||
|
code <- oneOf "bhioO"
|
||||||
|
many1 spacenonewline
|
||||||
|
datetime <- ledgerdatetime
|
||||||
|
comment <- optionMaybe (many1 spacenonewline >> liftM2 (++) getParentAccount restofline)
|
||||||
|
return $ TimeLogEntry (read [code]) datetime (fromMaybe "" comment)
|
||||||
|
|
||||||
|
tests_Timelog = TestList [
|
||||||
|
]
|
||||||
|
|
||||||
@ -32,15 +32,17 @@ library
|
|||||||
Hledger.Data.Amount
|
Hledger.Data.Amount
|
||||||
Hledger.Data.Commodity
|
Hledger.Data.Commodity
|
||||||
Hledger.Data.Dates
|
Hledger.Data.Dates
|
||||||
Hledger.Data.IO
|
|
||||||
Hledger.Data.Transaction
|
Hledger.Data.Transaction
|
||||||
Hledger.Data.Journal
|
Hledger.Data.Journal
|
||||||
Hledger.Data.Ledger
|
Hledger.Data.Ledger
|
||||||
Hledger.Data.Posting
|
Hledger.Data.Posting
|
||||||
Hledger.Data.Parse
|
|
||||||
Hledger.Data.TimeLog
|
Hledger.Data.TimeLog
|
||||||
Hledger.Data.Types
|
Hledger.Data.Types
|
||||||
Hledger.Data.Utils
|
Hledger.Data.Utils
|
||||||
|
Hledger.Read
|
||||||
|
Hledger.Read.Common
|
||||||
|
Hledger.Read.Journal
|
||||||
|
Hledger.Read.Timelog
|
||||||
Build-Depends:
|
Build-Depends:
|
||||||
base >= 3 && < 5
|
base >= 3 && < 5
|
||||||
,containers
|
,containers
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user