parsing: support D default commodity directive

This commit is contained in:
Simon Michael 2010-11-12 23:54:21 +00:00
parent 8429df0f32
commit 78db98366f
6 changed files with 102 additions and 38 deletions

View File

@ -797,6 +797,20 @@ You can pull in the content of additional journal files, by writing lines like t
The `!include` directive may only be used in journal files, and currently The `!include` directive may only be used in journal files, and currently
it may only include other journal files (eg, not timelog files.) it may only include other journal files (eg, not timelog files.)
##### Default commodity
You can set a default commodity with a `D` directive in the journal. This
will be used for any subsequent amounts with no commodity symbol,
including the commodity display settings (left or right symbol, spacing,
thousands separator, and precision.)
; default commodity: british pound, comma thousands separator, two decimal places
D £1,000.00
2010/1/1
a 2340.11 ; <- no commodity symbol, so will use the above
b
#### Default parent account #### Default parent account
You can specify a default parent account within a section of the journal with You can specify a default parent account within a section of the journal with

View File

@ -49,7 +49,7 @@ data Interval = NoInterval | Daily | Weekly | Monthly | Quarterly | Yearly
type AccountName = String type AccountName = String
data Side = L | R deriving (Eq,Show,Ord) data Side = L | R deriving (Eq,Show,Read,Ord)
data Commodity = Commodity { data Commodity = Commodity {
symbol :: String, -- ^ the commodity's symbol symbol :: String, -- ^ the commodity's symbol
@ -58,7 +58,7 @@ data Commodity = Commodity {
spaced :: Bool, -- ^ should there be a space between symbol and quantity spaced :: Bool, -- ^ should there be a space between symbol and quantity
comma :: Bool, -- ^ should thousands be comma-separated comma :: Bool, -- ^ should thousands be comma-separated
precision :: Int -- ^ number of decimal places to display precision :: Int -- ^ number of decimal places to display
} deriving (Eq,Show,Ord) } deriving (Eq,Show,Read,Ord)
data Amount = Amount { data Amount = Amount {
commodity :: Commodity, commodity :: Commodity,

View File

@ -10,7 +10,7 @@ where
import Control.Monad.Error import Control.Monad.Error
import Hledger.Data.Utils import Hledger.Data.Utils
import Hledger.Data.Types (Journal) import Hledger.Data.Types (Journal, Commodity)
import Hledger.Data.Journal import Hledger.Data.Journal
import System.Directory (getHomeDirectory) import System.Directory (getHomeDirectory)
import System.FilePath(takeDirectory,combine) import System.FilePath(takeDirectory,combine)
@ -44,12 +44,12 @@ parseJournalWith p f s = do
-- | Some state kept while parsing a journal file. -- | Some state kept while parsing a journal file.
data JournalContext = Ctx { data JournalContext = Ctx {
ctxYear :: !(Maybe Integer) -- ^ the default year most recently specified with Y ctxYear :: !(Maybe Integer) -- ^ the default year most recently specified with Y
, ctxCommod :: !(Maybe String) -- ^ I don't know , ctxCommodity :: !(Maybe Commodity) -- ^ the default commodity recently specified with D
, ctxAccount :: ![String] -- ^ the current stack of parent accounts specified by !account , ctxAccount :: ![String] -- ^ the current stack of parent accounts specified by !account
} deriving (Read, Show) } deriving (Read, Show)
emptyCtx :: JournalContext emptyCtx :: JournalContext
emptyCtx = Ctx { ctxYear = Nothing, ctxCommod = Nothing, ctxAccount = [] } emptyCtx = Ctx { ctxYear = Nothing, ctxCommodity = Nothing, ctxAccount = [] }
setYear :: Integer -> GenParser tok JournalContext () setYear :: Integer -> GenParser tok JournalContext ()
setYear y = updateState (\ctx -> ctx{ctxYear=Just y}) setYear y = updateState (\ctx -> ctx{ctxYear=Just y})
@ -57,6 +57,12 @@ setYear y = updateState (\ctx -> ctx{ctxYear=Just y})
getYear :: GenParser tok JournalContext (Maybe Integer) getYear :: GenParser tok JournalContext (Maybe Integer)
getYear = liftM ctxYear getState getYear = liftM ctxYear getState
setCommodity :: Commodity -> GenParser tok JournalContext ()
setCommodity c = updateState (\ctx -> ctx{ctxCommodity=Just c})
getCommodity :: GenParser tok JournalContext (Maybe Commodity)
getCommodity = liftM ctxCommodity getState
pushParentAccount :: String -> GenParser tok JournalContext () pushParentAccount :: String -> GenParser tok JournalContext ()
pushParentAccount parent = updateState addParentAccount pushParentAccount parent = updateState addParentAccount
where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 } where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 }

View File

@ -169,6 +169,7 @@ journalFile = do journalupdates <- many journalItem
, liftM (return . addPeriodicTransaction) ledgerPeriodicTransaction , liftM (return . addPeriodicTransaction) ledgerPeriodicTransaction
, liftM (return . addHistoricalPrice) ledgerHistoricalPrice , liftM (return . addHistoricalPrice) ledgerHistoricalPrice
, ledgerDefaultYear , ledgerDefaultYear
, ledgerDefaultCommodity
, ledgerIgnoredPriceCommodity , ledgerIgnoredPriceCommodity
, ledgerTagDirective , ledgerTagDirective
, ledgerEndTagDirective , ledgerEndTagDirective
@ -178,20 +179,20 @@ journalFile = do journalupdates <- many journalItem
journalAddFilePath :: FilePath -> Journal -> Journal journalAddFilePath :: FilePath -> Journal -> Journal
journalAddFilePath f j@Journal{allfilepaths=fs} = j{allfilepaths=fs++[f]} journalAddFilePath f j@Journal{allfilepaths=fs} = j{allfilepaths=fs++[f]}
emptyLine :: GenParser Char st () emptyLine :: GenParser Char JournalContext ()
emptyLine = do many spacenonewline emptyLine = do many spacenonewline
optional $ (char ';' <?> "comment") >> many (noneOf "\n") optional $ (char ';' <?> "comment") >> many (noneOf "\n")
newline newline
return () return ()
ledgercomment :: GenParser Char st String ledgercomment :: GenParser Char JournalContext String
ledgercomment = do ledgercomment = do
many1 $ char ';' many1 $ char ';'
many spacenonewline many spacenonewline
many (noneOf "\n") many (noneOf "\n")
<?> "comment" <?> "comment"
ledgercommentline :: GenParser Char st String ledgercommentline :: GenParser Char JournalContext String
ledgercommentline = do ledgercommentline = do
many spacenonewline many spacenonewline
s <- ledgercomment s <- ledgercomment
@ -272,14 +273,6 @@ ledgerIgnoredPriceCommodity = do
restofline restofline
return $ return id return $ return id
ledgerDefaultCommodity :: GenParser Char JournalContext JournalUpdate
ledgerDefaultCommodity = do
char 'D' <?> "default commodity"
many1 spacenonewline
someamount
restofline
return $ return id
ledgerCommodityConversion :: GenParser Char JournalContext JournalUpdate ledgerCommodityConversion :: GenParser Char JournalContext JournalUpdate
ledgerCommodityConversion = do ledgerCommodityConversion = do
char 'C' <?> "commodity conversion" char 'C' <?> "commodity conversion"
@ -317,6 +310,17 @@ ledgerDefaultYear = do
setYear y' setYear y'
return $ return id return $ return id
ledgerDefaultCommodity :: GenParser Char JournalContext JournalUpdate
ledgerDefaultCommodity = do
char 'D' <?> "default commodity"
many1 spacenonewline
a <- someamount
-- someamount always returns a MixedAmount containing one Amount, but let's be safe
let as = amounts a
when (not $ null as) $ setCommodity $ commodity $ head as
restofline
return $ return id
-- | Try to parse a ledger entry. If we successfully parse an entry, -- | Try to parse a ledger entry. If we successfully parse an entry,
-- check it can be balanced, and fail if not. -- check it can be balanced, and fail if not.
ledgerTransaction :: GenParser Char JournalContext Transaction ledgerTransaction :: GenParser Char JournalContext Transaction
@ -384,10 +388,10 @@ ledgereffectivedate actualdate = do
edate <- withDefaultYear actualdate ledgerdate edate <- withDefaultYear actualdate ledgerdate
return edate return edate
ledgerstatus :: GenParser Char st Bool ledgerstatus :: GenParser Char JournalContext Bool
ledgerstatus = try (do { many1 spacenonewline; char '*' <?> "status"; return True } ) <|> return False ledgerstatus = try (do { many1 spacenonewline; char '*' <?> "status"; return True } ) <|> return False
ledgercode :: GenParser Char st String ledgercode :: GenParser Char JournalContext String
ledgercode = try (do { many1 spacenonewline; char '(' <?> "code"; code <- anyChar `manyTill` char ')'; return code } ) <|> return "" ledgercode = try (do { many1 spacenonewline; char '(' <?> "code"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""
ledgerpostings :: GenParser Char JournalContext [Posting] ledgerpostings :: GenParser Char JournalContext [Posting]
@ -404,7 +408,7 @@ ledgerpostings = do
return $ map (fromparse . parseWithCtx ctx (setPosition pos >> ledgerposting)) ls' return $ map (fromparse . parseWithCtx ctx (setPosition pos >> ledgerposting)) ls'
<?> "postings" <?> "postings"
linebeginningwithspaces :: GenParser Char st String linebeginningwithspaces :: GenParser Char JournalContext String
linebeginningwithspaces = do linebeginningwithspaces = do
sp <- many1 spacenonewline sp <- many1 spacenonewline
c <- nonspace c <- nonspace
@ -448,17 +452,17 @@ ledgeraccountname = do
-- | Parse an amount, with an optional left or right currency symbol and -- | Parse an amount, with an optional left or right currency symbol and
-- optional price. -- optional price.
postingamount :: GenParser Char st MixedAmount postingamount :: GenParser Char JournalContext MixedAmount
postingamount = postingamount =
try (do try (do
many1 spacenonewline many1 spacenonewline
someamount <|> return missingamt someamount <|> return missingamt
) <|> return missingamt ) <|> return missingamt
someamount :: GenParser Char st MixedAmount someamount :: GenParser Char JournalContext MixedAmount
someamount = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount someamount = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount
leftsymbolamount :: GenParser Char st MixedAmount leftsymbolamount :: GenParser Char JournalContext MixedAmount
leftsymbolamount = do leftsymbolamount = do
sign <- optionMaybe $ string "-" sign <- optionMaybe $ string "-"
let applysign = if isJust sign then negate else id let applysign = if isJust sign then negate else id
@ -470,7 +474,7 @@ leftsymbolamount = do
return $ applysign $ Mixed [Amount c q pri] return $ applysign $ Mixed [Amount c q pri]
<?> "left-symbol amount" <?> "left-symbol amount"
rightsymbolamount :: GenParser Char st MixedAmount rightsymbolamount :: GenParser Char JournalContext MixedAmount
rightsymbolamount = do rightsymbolamount = do
(q,p,comma) <- amountquantity (q,p,comma) <- amountquantity
sp <- many spacenonewline sp <- many spacenonewline
@ -480,28 +484,29 @@ rightsymbolamount = do
return $ Mixed [Amount c q pri] return $ Mixed [Amount c q pri]
<?> "right-symbol amount" <?> "right-symbol amount"
nosymbolamount :: GenParser Char st MixedAmount nosymbolamount :: GenParser Char JournalContext MixedAmount
nosymbolamount = do nosymbolamount = do
(q,p,comma) <- amountquantity (q,p,comma) <- amountquantity
pri <- priceamount pri <- priceamount
let c = Commodity {symbol="",side=L,spaced=False,comma=comma,precision=p} defc <- getCommodity
let c = fromMaybe Commodity{symbol="",side=L,spaced=False,comma=comma,precision=p} defc
return $ Mixed [Amount c q pri] return $ Mixed [Amount c q pri]
<?> "no-symbol amount" <?> "no-symbol amount"
commoditysymbol :: GenParser Char st String commoditysymbol :: GenParser Char JournalContext String
commoditysymbol = (quotedcommoditysymbol <|> simplecommoditysymbol) <?> "commodity symbol" commoditysymbol = (quotedcommoditysymbol <|> simplecommoditysymbol) <?> "commodity symbol"
quotedcommoditysymbol :: GenParser Char st String quotedcommoditysymbol :: GenParser Char JournalContext String
quotedcommoditysymbol = do quotedcommoditysymbol = do
char '"' char '"'
s <- many1 $ noneOf ";\n\"" s <- many1 $ noneOf ";\n\""
char '"' char '"'
return s return s
simplecommoditysymbol :: GenParser Char st String simplecommoditysymbol :: GenParser Char JournalContext String
simplecommoditysymbol = many1 (noneOf nonsimplecommoditychars) simplecommoditysymbol = many1 (noneOf nonsimplecommoditychars)
priceamount :: GenParser Char st (Maybe MixedAmount) priceamount :: GenParser Char JournalContext (Maybe MixedAmount)
priceamount = priceamount =
try (do try (do
many spacenonewline many spacenonewline
@ -516,7 +521,7 @@ priceamount =
-- | Parse a ledger-style numeric quantity and also return the number of -- | Parse a ledger-style numeric quantity and also return the number of
-- digits to the right of the decimal point and whether thousands are -- digits to the right of the decimal point and whether thousands are
-- separated by comma. -- separated by comma.
amountquantity :: GenParser Char st (Double, Int, Bool) amountquantity :: GenParser Char JournalContext (Double, Int, Bool)
amountquantity = do amountquantity = do
sign <- optionMaybe $ string "-" sign <- optionMaybe $ string "-"
(intwithcommas,frac) <- numberparts (intwithcommas,frac) <- numberparts
@ -534,10 +539,10 @@ amountquantity = do
-- | parse the two strings of digits before and after a possible decimal -- | parse the two strings of digits before and after a possible decimal
-- point. The integer part may contain commas, or either part may be -- point. The integer part may contain commas, or either part may be
-- empty, or there may be no point. -- empty, or there may be no point.
numberparts :: GenParser Char st (String,String) numberparts :: GenParser Char JournalContext (String,String)
numberparts = numberpartsstartingwithdigit <|> numberpartsstartingwithpoint numberparts = numberpartsstartingwithdigit <|> numberpartsstartingwithpoint
numberpartsstartingwithdigit :: GenParser Char st (String,String) numberpartsstartingwithdigit :: GenParser Char JournalContext (String,String)
numberpartsstartingwithdigit = do numberpartsstartingwithdigit = do
let digitorcomma = digit <|> char ',' let digitorcomma = digit <|> char ','
first <- digit first <- digit
@ -545,7 +550,7 @@ numberpartsstartingwithdigit = do
frac <- try (do {char '.'; many digit}) <|> return "" frac <- try (do {char '.'; many digit}) <|> return ""
return (first:rest,frac) return (first:rest,frac)
numberpartsstartingwithpoint :: GenParser Char st (String,String) numberpartsstartingwithpoint :: GenParser Char JournalContext (String,String)
numberpartsstartingwithpoint = do numberpartsstartingwithpoint = do
char '.' char '.'
frac <- many1 digit frac <- many1 digit
@ -618,7 +623,7 @@ tests_Journal = TestList [
let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity
assertMixedAmountParse parseresult mixedamount = assertMixedAmountParse parseresult mixedamount =
(either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount) (either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)
assertMixedAmountParse (parsewith someamount "1 @ $2") assertMixedAmountParse (parseWithCtx emptyCtx someamount "1 @ $2")
(Mixed [Amount unknown 1 (Just $ Mixed [Amount dollar{precision=0} 2 Nothing])]) (Mixed [Amount unknown 1 (Just $ Mixed [Amount dollar{precision=0} 2 Nothing])])
,"postingamount" ~: do ,"postingamount" ~: do

View File

@ -9,6 +9,7 @@ module Hledger.Cli.Commands.Add
where where
import Hledger.Data import Hledger.Data
import Hledger.Read.Journal (someamount) import Hledger.Read.Journal (someamount)
import Hledger.Read.Common (emptyCtx)
import Hledger.Cli.Options import Hledger.Cli.Options
import Hledger.Cli.Commands.Register (registerReport, registerReportAsText) import Hledger.Cli.Commands.Register (registerReport, registerReportAsText)
#if __GLASGOW_HASKELL__ <= 610 #if __GLASGOW_HASKELL__ <= 610
@ -92,7 +93,7 @@ getPostings accept historicalps enteredps = do
then return enteredps then return enteredps
else do else do
amountstr <- askFor (printf "amount %d" n) defaultamount validateamount amountstr <- askFor (printf "amount %d" n) defaultamount validateamount
let amount = fromparse $ parse (someamount <|> return missingamt) "" amountstr let amount = fromparse $ runParser (someamount <|> return missingamt) emptyCtx "" amountstr
let p = nullposting{paccount=stripbrackets account, let p = nullposting{paccount=stripbrackets account,
pamount=amount, pamount=amount,
ptype=postingtype account} ptype=postingtype account}
@ -113,7 +114,7 @@ getPostings accept historicalps enteredps = do
postingtype _ = RegularPosting postingtype _ = RegularPosting
stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse
validateamount = Just $ \s -> (null s && not (null enteredrealps)) validateamount = Just $ \s -> (null s && not (null enteredrealps))
|| isRight (parse (someamount>>many spacenonewline>>eof) "" s) || isRight (runParser (someamount>>many spacenonewline>>eof) emptyCtx "" s)
-- | Prompt for and read a string value, optionally with a default value -- | Prompt for and read a string value, optionally with a default value
-- and a validator. A validator causes the prompt to repeat until the -- and a validator. A validator causes the prompt to repeat until the

View File

@ -0,0 +1,38 @@
# a default commodity defined with the D directive will be used for any
# commodity-less amounts in subsequent transactions.
#
bin/hledger -f- print
<<<
; no default commodity
2010/1/1 x
a 1000
b
; pound, two decimal places, no thousands separator
D £1000.00
2010/1/1 y
a 1000
b
; dollar, no decimal places, comma thousands separator
D $1,000
2010/1/1 z
a 1000
b
>>>
2010/01/01 x
a 1000
b -1000
2010/01/01 y
a £1000.00
b £-1000.00
2010/01/01 z
a $1,000
b $-1,000