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