journal: require a decimal point in commodity/format/D directives

A commodity directive that doesn't specify the decimal point character
increases ambiguity and the chance of misparsing numbers, especially
as it overrides all style information inferred from the journal amounts.
In some cases it caused amounts with a decimal point to be parsed as if
with a digit group separator so 1.234 became 1234.

We could augment it with extra info from the journal amounts, when available,
but it would still be possible to be ambiguous, and that won't be obvious.

A commodity directive is what we recommend to nail down the style.
It seems the simple and really only way to do this reliably is to require
an explicit decimal point character. Most folks probably do this already.

Unfortunately, it makes another potential incompatiblity with ledger and
beancount journals. But the error message will be clear and easy to
work around.
This commit is contained in:
Simon Michael 2018-04-20 21:56:06 -07:00
parent 3f2827424c
commit 0b380971f7
4 changed files with 41 additions and 31 deletions

View File

@ -737,18 +737,19 @@ journalApplyCommodityStyles j@Journal{jtxns=ts, jmarketprices=mps} = j''
fixposting p@Posting{pamount=a} = p{pamount=styleMixedAmount styles a} fixposting p@Posting{pamount=a} = p{pamount=styleMixedAmount styles a}
fixmarketprice mp@MarketPrice{mpamount=a} = mp{mpamount=styleAmount styles a} fixmarketprice mp@MarketPrice{mpamount=a} = mp{mpamount=styleAmount styles a}
-- | Get all the amount styles defined in this journal, either -- | Get all the amount styles defined in this journal, either declared by
-- declared by a commodity directive (preferred) or inferred from amounts, -- a commodity directive or inferred from amounts, as a map from symbol to style.
-- as a map from symbol to style. -- Styles declared by commodity directives take precedence, and these also are
-- guaranteed to know their decimal point character.
journalCommodityStyles :: Journal -> M.Map CommoditySymbol AmountStyle journalCommodityStyles :: Journal -> M.Map CommoditySymbol AmountStyle
journalCommodityStyles j = declaredstyles <> inferredstyles journalCommodityStyles j = declaredstyles <> inferredstyles
where where
declaredstyles = M.mapMaybe cformat $ jcommodities j declaredstyles = M.mapMaybe cformat $ jcommodities j
inferredstyles = jinferredcommodities j inferredstyles = jinferredcommodities j
-- | Infer a display format for each commodity based on the amounts parsed. -- | Collect and save inferred amount styles for each commodity based on
-- "hledger... will use the format of the first posting amount in the -- the posting amounts in that commodity (excluding price amounts), ie:
-- commodity, and the highest precision of all posting amounts in the commodity." -- "the format of the first amount, adjusted to the highest precision of all amounts".
journalInferCommodityStyles :: Journal -> Journal journalInferCommodityStyles :: Journal -> Journal
journalInferCommodityStyles j = journalInferCommodityStyles j =
j{jinferredcommodities = j{jinferredcommodities =

View File

@ -274,15 +274,21 @@ commoditydirectivep = try commoditydirectiveonelinep <|> commoditydirectivemulti
-- --
-- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00" -- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00"
-- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00 ; blah\n" -- >>> Right _ <- rejp commoditydirectiveonelinep "commodity $1.00 ; blah\n"
commoditydirectiveonelinep :: Monad m => JournalParser m () commoditydirectiveonelinep :: Monad m => ErroringJournalParser m ()
commoditydirectiveonelinep = do commoditydirectiveonelinep = do
string "commodity" string "commodity"
lift (skipSome spacenonewline) lift (skipSome spacenonewline)
pos <- getPosition
Amount{acommodity,astyle} <- amountp Amount{acommodity,astyle} <- amountp
lift (skipMany spacenonewline) lift (skipMany spacenonewline)
_ <- followingcommentp <|> (lift eolof >> return "") _ <- followingcommentp <|> (lift eolof >> return "")
let comm = Commodity{csymbol=acommodity, cformat=Just astyle} let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg2 "style from commodity directive" astyle}
modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j}) if asdecimalpoint astyle == Nothing
then parserErrorAt pos pleaseincludedecimalpoint
else modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j})
pleaseincludedecimalpoint :: String
pleaseincludedecimalpoint = "to avoid ambiguity, please include a decimal point in commodity directives"
-- | Parse a multi-line commodity directive, containing 0 or more format subdirectives. -- | Parse a multi-line commodity directive, containing 0 or more format subdirectives.
-- --
@ -309,7 +315,10 @@ formatdirectivep expectedsym = do
Amount{acommodity,astyle} <- amountp Amount{acommodity,astyle} <- amountp
_ <- followingcommentp <|> (lift eolof >> return "") _ <- followingcommentp <|> (lift eolof >> return "")
if acommodity==expectedsym if acommodity==expectedsym
then return astyle then
if asdecimalpoint astyle == Nothing
then parserErrorAt pos pleaseincludedecimalpoint
else return $ dbg2 "style from format subdirective" astyle
else parserErrorAt pos $ else parserErrorAt pos $
printf "commodity directive symbol \"%s\" and format directive symbol \"%s\" should be the same" expectedsym acommodity printf "commodity directive symbol \"%s\" and format directive symbol \"%s\" should be the same" expectedsym acommodity
@ -395,13 +404,16 @@ defaultyeardirectivep = do
failIfInvalidYear y failIfInvalidYear y
setYear y' setYear y'
defaultcommoditydirectivep :: Monad m => JournalParser m () defaultcommoditydirectivep :: Monad m => ErroringJournalParser m ()
defaultcommoditydirectivep = do defaultcommoditydirectivep = do
char 'D' <?> "default commodity" char 'D' <?> "default commodity"
lift (skipSome spacenonewline) lift (skipSome spacenonewline)
Amount{..} <- amountp pos <- getPosition
Amount{acommodity,astyle} <- amountp
lift restofline lift restofline
setDefaultCommodityAndStyle (acommodity, astyle) if asdecimalpoint astyle == Nothing
then parserErrorAt pos pleaseincludedecimalpoint
else setDefaultCommodityAndStyle (acommodity, astyle)
marketpricedirectivep :: Monad m => JournalParser m MarketPrice marketpricedirectivep :: Monad m => JournalParser m MarketPrice
marketpricedirectivep = do marketpricedirectivep = do

View File

@ -799,9 +799,7 @@ line containing just `end comment` ends it. See [comments](#comments).
### commodity directive ### commodity directive
The `commodity` directive predefines commodities (currently this is just informational), The `commodity` directive declares commodities which may be used in the journal (though currently we do not enforce this).
and also it may define the display format for amounts in this commodity (overriding the automatically inferred format).
It may be written on a single line, like this: It may be written on a single line, like this:
```journal ```journal
@ -827,6 +825,12 @@ commodity INR
format INR 9,99,99,999.00 format INR 9,99,99,999.00
``` ```
Commodity directives have a second purpose: they define the standard display format for amounts in the commodity.
Normally the display format is inferred from journal entries, but this can be unpredictable;
declaring it with a commodity directive overrides this and removes ambiguity.
Towards this end, amounts in commodity directives must always be written with a decimal point
(a period or comma, followed by 0 or more decimal digits).
### Default commodity ### Default commodity
The D directive sets a default commodity (and display format), to be used for amounts without a commodity symbol (ie, plain numbers). The D directive sets a default commodity (and display format), to be used for amounts without a commodity symbol (ie, plain numbers).
@ -843,6 +847,8 @@ D $1,000.00
b b
``` ```
As with the `commodity` directive, the amount must always be written with a decimal point.
### Default year ### Default year
You can set a default year to be used for subsequent dates which don't You can set a default year to be used for subsequent dates which don't

View File

@ -126,26 +126,17 @@ commodity €1,000.00
>>>2 >>>2
>>>=0 >>>=0
# 11. No decimals but have hint from commodity directive with groups # 11. Commodity directive requires a decimal point
hledger bal -f - hledger bal -f -
<<< <<<
commodity 1,000,000 EUR commodity 1000 EUR
>>>2 /please include a decimal point/
>>>=1
2017/1/1 # 12. Commodity directive with zero precision
a 1,000 EUR
b -1,000.00 EUR
>>>
1,000 EUR a
-1,000 EUR b
--------------------
0
>>>2
>>>=0
# 12. No decimals but have hint from commodity directive with zero precision
hledger bal -f - hledger bal -f -
<<< <<<
commodity 100 EUR commodity 100. EUR
2017/1/1 2017/1/1
a 1,000 EUR a 1,000 EUR