csv: decimal-mark rule to help with number parsing
Journal keeps a new piece of parsing state, a decimal mark character, which can optionally be set to force the number format expected by all amount parsers.
This commit is contained in:
		
							parent
							
								
									4242a8592a
								
							
						
					
					
						commit
						524e23bc37
					
				| @ -175,6 +175,7 @@ instance Semigroup Journal where | |||||||
|   j1 <> j2 = Journal { |   j1 <> j2 = Journal { | ||||||
|      jparsedefaultyear          = jparsedefaultyear          j2 |      jparsedefaultyear          = jparsedefaultyear          j2 | ||||||
|     ,jparsedefaultcommodity     = jparsedefaultcommodity     j2 |     ,jparsedefaultcommodity     = jparsedefaultcommodity     j2 | ||||||
|  |     ,jparsedecimalmark          = jparsedecimalmark          j2 | ||||||
|     ,jparseparentaccounts       = jparseparentaccounts       j2 |     ,jparseparentaccounts       = jparseparentaccounts       j2 | ||||||
|     ,jparsealiases              = jparsealiases              j2 |     ,jparsealiases              = jparsealiases              j2 | ||||||
|     -- ,jparsetransactioncount     = jparsetransactioncount     j1 +  jparsetransactioncount     j2 |     -- ,jparsetransactioncount     = jparsetransactioncount     j1 +  jparsetransactioncount     j2 | ||||||
| @ -201,6 +202,7 @@ nulljournal :: Journal | |||||||
| nulljournal = Journal { | nulljournal = Journal { | ||||||
|    jparsedefaultyear          = Nothing |    jparsedefaultyear          = Nothing | ||||||
|   ,jparsedefaultcommodity     = Nothing |   ,jparsedefaultcommodity     = Nothing | ||||||
|  |   ,jparsedecimalmark          = Nothing | ||||||
|   ,jparseparentaccounts       = [] |   ,jparseparentaccounts       = [] | ||||||
|   ,jparsealiases              = [] |   ,jparsealiases              = [] | ||||||
|   -- ,jparsetransactioncount     = 0 |   -- ,jparsetransactioncount     = 0 | ||||||
|  | |||||||
| @ -159,6 +159,12 @@ data AccountAlias = BasicAlias AccountName AccountName | |||||||
| 
 | 
 | ||||||
| data Side = L | R deriving (Eq,Show,Read,Ord,Generic) | data Side = L | R deriving (Eq,Show,Read,Ord,Generic) | ||||||
| 
 | 
 | ||||||
|  | -- | One of the decimal marks we support: either period or comma. | ||||||
|  | type DecimalMark = Char | ||||||
|  | 
 | ||||||
|  | isDecimalMark :: Char -> Bool | ||||||
|  | isDecimalMark c = c == '.' || c == ',' | ||||||
|  | 
 | ||||||
| -- | The basic numeric type used in amounts. | -- | The basic numeric type used in amounts. | ||||||
| type Quantity = Decimal | type Quantity = Decimal | ||||||
| -- The following is for hledger-web, and requires blaze-markup. | -- The following is for hledger-web, and requires blaze-markup. | ||||||
| @ -440,6 +446,7 @@ data Journal = Journal { | |||||||
|   -- parsing-related data |   -- parsing-related data | ||||||
|    jparsedefaultyear      :: Maybe Year                            -- ^ the current default year, specified by the most recent Y directive (or current date) |    jparsedefaultyear      :: Maybe Year                            -- ^ the current default year, specified by the most recent Y directive (or current date) | ||||||
|   ,jparsedefaultcommodity :: Maybe (CommoditySymbol,AmountStyle)   -- ^ the current default commodity and its format, specified by the most recent D directive |   ,jparsedefaultcommodity :: Maybe (CommoditySymbol,AmountStyle)   -- ^ the current default commodity and its format, specified by the most recent D directive | ||||||
|  |   ,jparsedecimalmark      :: Maybe DecimalMark                     -- ^ the character to always parse as decimal point, if set by CsvReader's decimal-mark (or a future journal directive) | ||||||
|   ,jparseparentaccounts   :: [AccountName]                         -- ^ the current stack of parent account names, specified by apply account directives |   ,jparseparentaccounts   :: [AccountName]                         -- ^ the current stack of parent account names, specified by apply account directives | ||||||
|   ,jparsealiases          :: [AccountAlias]                        -- ^ the current account name aliases in effect, specified by alias directives (& options ?) |   ,jparsealiases          :: [AccountAlias]                        -- ^ the current account name aliases in effect, specified by alias directives (& options ?) | ||||||
|   -- ,jparsetransactioncount :: Integer                               -- ^ the current count of transactions parsed so far (only journal format txns, currently) |   -- ,jparsetransactioncount :: Integer                               -- ^ the current count of transactions parsed so far (only journal format txns, currently) | ||||||
|  | |||||||
| @ -349,6 +349,15 @@ setYear y = modify' (\j -> j{jparsedefaultyear=Just y}) | |||||||
| getYear :: JournalParser m (Maybe Year) | getYear :: JournalParser m (Maybe Year) | ||||||
| getYear = fmap jparsedefaultyear get | getYear = fmap jparsedefaultyear get | ||||||
| 
 | 
 | ||||||
|  | -- | Get the decimal mark that has been specified for parsing, if any | ||||||
|  | -- (eg by the CSV decimal-mark rule, or possibly a future journal directive). | ||||||
|  | -- Return it as an AmountStyle that amount parsers can use. | ||||||
|  | getDecimalMarkStyle :: JournalParser m (Maybe AmountStyle) | ||||||
|  | getDecimalMarkStyle = do | ||||||
|  |   Journal{jparsedecimalmark} <- get | ||||||
|  |   let mdecmarkStyle = maybe Nothing (\c -> Just $ amountstyle{asdecimalpoint=Just c}) jparsedecimalmark | ||||||
|  |   return mdecmarkStyle | ||||||
|  | 
 | ||||||
| setDefaultCommodityAndStyle :: (CommoditySymbol,AmountStyle) -> JournalParser m () | setDefaultCommodityAndStyle :: (CommoditySymbol,AmountStyle) -> JournalParser m () | ||||||
| setDefaultCommodityAndStyle cs = modify' (\j -> j{jparsedefaultcommodity=Just cs}) | setDefaultCommodityAndStyle cs = modify' (\j -> j{jparsedefaultcommodity=Just cs}) | ||||||
| 
 | 
 | ||||||
| @ -640,9 +649,26 @@ spaceandamountormissingp = | |||||||
| -- or right, followed by, in any order: an optional transaction price, | -- or right, followed by, in any order: an optional transaction price, | ||||||
| -- an optional ledger-style lot price, and/or an optional ledger-style | -- an optional ledger-style lot price, and/or an optional ledger-style | ||||||
| -- lot date. A lot price and lot date will be ignored. | -- lot date. A lot price and lot date will be ignored. | ||||||
|  | -- | ||||||
|  | -- To parse the amount's quantity (number) we need to know which character  | ||||||
|  | -- represents a decimal mark. We find it in one of three ways: | ||||||
|  | -- | ||||||
|  | -- 1. If a decimal mark has been set explicitly in the journal parse state,  | ||||||
|  | --    we use that | ||||||
|  | -- | ||||||
|  | -- 2. Or if the journal has a commodity declaration for the amount's commodity, | ||||||
|  | --    we get the decimal mark from  that | ||||||
|  | -- | ||||||
|  | -- 3. Otherwise we will parse any valid decimal mark appearing in the | ||||||
|  | --    number, as long as the number appears well formed. | ||||||
|  | -- | ||||||
|  | -- Note 3 is the default zero-config case; it means we automatically handle | ||||||
|  | -- files with any supported decimal mark, but it also allows different decimal marks | ||||||
|  | -- in  different amounts, which is a bit too loose. There's an open issue. | ||||||
| amountp :: JournalParser m Amount | amountp :: JournalParser m Amount | ||||||
| amountp = label "amount" $ do | amountp = label "amount" $ do | ||||||
|   let spaces = lift $ skipNonNewlineSpaces |   let  | ||||||
|  |     spaces = lift $ skipNonNewlineSpaces | ||||||
|   amount <- amountwithoutpricep <* spaces |   amount <- amountwithoutpricep <* spaces | ||||||
|   (mprice, _elotprice, _elotdate) <- runPermutation $ |   (mprice, _elotprice, _elotdate) <- runPermutation $ | ||||||
|     (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp <* spaces) |     (,,) <$> toPermutationWithDefault Nothing (Just <$> priceamountp <* spaces) | ||||||
| @ -650,9 +676,8 @@ amountp = label "amount" $ do | |||||||
|          <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces) |          <*> toPermutationWithDefault Nothing (Just <$> lotdatep <* spaces) | ||||||
|   pure $ amount { aprice = mprice } |   pure $ amount { aprice = mprice } | ||||||
| 
 | 
 | ||||||
| -- XXX Just like amountp but don't allow lot prices. Needed for balanceassertionp. | amountpnolotpricesp :: JournalParser m Amount | ||||||
| amountpnolotprices :: JournalParser m Amount | amountpnolotpricesp = label "amount" $ do | ||||||
| amountpnolotprices = label "amount" $ do |  | ||||||
|   let spaces = lift $ skipNonNewlineSpaces |   let spaces = lift $ skipNonNewlineSpaces | ||||||
|   amount <- amountwithoutpricep |   amount <- amountwithoutpricep | ||||||
|   spaces |   spaces | ||||||
| @ -669,7 +694,9 @@ amountwithoutpricep = do | |||||||
|   leftsymbolamountp :: Bool -> (Decimal -> Decimal) -> JournalParser m Amount |   leftsymbolamountp :: Bool -> (Decimal -> Decimal) -> JournalParser m Amount | ||||||
|   leftsymbolamountp mult sign = label "amount" $ do |   leftsymbolamountp mult sign = label "amount" $ do | ||||||
|     c <- lift commoditysymbolp |     c <- lift commoditysymbolp | ||||||
|     suggestedStyle <- getAmountStyle c |     mdecmarkStyle <- getDecimalMarkStyle | ||||||
|  |     mcommodityStyle <- getAmountStyle c | ||||||
|  |     let suggestedStyle = mdecmarkStyle <|> mcommodityStyle | ||||||
|     commodityspaced <- lift skipNonNewlineSpaces' |     commodityspaced <- lift skipNonNewlineSpaces' | ||||||
|     sign2 <- lift $ signp |     sign2 <- lift $ signp | ||||||
|     offBeforeNum <- getOffset |     offBeforeNum <- getOffset | ||||||
| @ -692,14 +719,18 @@ amountwithoutpricep = do | |||||||
|     case mSpaceAndCommodity of |     case mSpaceAndCommodity of | ||||||
|       -- right symbol amount |       -- right symbol amount | ||||||
|       Just (commodityspaced, c) -> do |       Just (commodityspaced, c) -> do | ||||||
|         suggestedStyle <- getAmountStyle c |         mdecmarkStyle <- getDecimalMarkStyle | ||||||
|         (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent |         mcommodityStyle <- getAmountStyle c | ||||||
|  |         let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle | ||||||
|  |         (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent | ||||||
|         let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps} |         let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps} | ||||||
|         return $ nullamt{acommodity=c, aquantity=sign q, aismultiplier=mult, astyle=s, aprice=Nothing} |         return $ nullamt{acommodity=c, aquantity=sign q, aismultiplier=mult, astyle=s, aprice=Nothing} | ||||||
|       -- no symbol amount |       -- no symbol amount | ||||||
|       Nothing -> do |       Nothing -> do | ||||||
|         suggestedStyle <- getDefaultAmountStyle |         mdecmarkStyle <- getDecimalMarkStyle | ||||||
|         (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent |         mcommodityStyle <- getDefaultAmountStyle | ||||||
|  |         let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle | ||||||
|  |         (q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent | ||||||
|         -- if a default commodity has been set, apply it and its style to this amount |         -- if a default commodity has been set, apply it and its style to this amount | ||||||
|         -- (unless it's a multiplier in an automated posting) |         -- (unless it's a multiplier in an automated posting) | ||||||
|         defcs <- getDefaultCommodityAndStyle |         defcs <- getDefaultCommodityAndStyle | ||||||
| @ -716,8 +747,8 @@ amountwithoutpricep = do | |||||||
|     -> Either AmbiguousNumber RawNumber |     -> Either AmbiguousNumber RawNumber | ||||||
|     -> Maybe Integer |     -> Maybe Integer | ||||||
|     -> TextParser m (Quantity, AmountPrecision, Maybe Char, Maybe DigitGroupStyle) |     -> TextParser m (Quantity, AmountPrecision, Maybe Char, Maybe DigitGroupStyle) | ||||||
|   interpretNumber posRegion suggestedStyle ambiguousNum mExp = |   interpretNumber posRegion msuggestedStyle ambiguousNum mExp = | ||||||
|     let rawNum = either (disambiguateNumber suggestedStyle) id ambiguousNum |     let rawNum = either (disambiguateNumber msuggestedStyle) id ambiguousNum | ||||||
|     in  case fromRawNumber rawNum mExp of |     in  case fromRawNumber rawNum mExp of | ||||||
|           Left errMsg -> customFailure $ |           Left errMsg -> customFailure $ | ||||||
|                            uncurry parseErrorAtRegion posRegion errMsg |                            uncurry parseErrorAtRegion posRegion errMsg | ||||||
| @ -776,7 +807,7 @@ balanceassertionp = do | |||||||
|   lift skipNonNewlineSpaces |   lift skipNonNewlineSpaces | ||||||
|   -- this amount can have a price; balance assertions ignore it, |   -- this amount can have a price; balance assertions ignore it, | ||||||
|   -- but balance assignments will use it |   -- but balance assignments will use it | ||||||
|   a <- amountpnolotprices <?> "amount (for a balance assertion or assignment)" |   a <- amountpnolotpricesp <?> "amount (for a balance assertion or assignment)" | ||||||
|   return BalanceAssertion |   return BalanceAssertion | ||||||
|     { baamount    = a |     { baamount    = a | ||||||
|     , batotal     = istotal |     , batotal     = istotal | ||||||
| @ -884,13 +915,12 @@ fromRawNumber raw mExp = do | |||||||
|       (a:b:cs) | a < b -> b:cs |       (a:b:cs) | a < b -> b:cs | ||||||
|       gs               -> gs |       gs               -> gs | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| disambiguateNumber :: Maybe AmountStyle -> AmbiguousNumber -> RawNumber | disambiguateNumber :: Maybe AmountStyle -> AmbiguousNumber -> RawNumber | ||||||
| disambiguateNumber suggestedStyle (AmbiguousNumber grp1 sep grp2) = | disambiguateNumber msuggestedStyle (AmbiguousNumber grp1 sep grp2) = | ||||||
|   -- If present, use the suggested style to disambiguate; |   -- If present, use the suggested style to disambiguate; | ||||||
|   -- otherwise, assume that the separator is a decimal point where possible. |   -- otherwise, assume that the separator is a decimal point where possible. | ||||||
|   if isDecimalPointChar sep && |   if isDecimalMark sep && | ||||||
|      maybe True (sep `isValidDecimalBy`) suggestedStyle |      maybe True (sep `isValidDecimalBy`) msuggestedStyle | ||||||
|   then NoSeparators grp1 (Just (sep, grp2)) |   then NoSeparators grp1 (Just (sep, grp2)) | ||||||
|   else WithSeparators sep [grp1, grp2] Nothing |   else WithSeparators sep [grp1, grp2] Nothing | ||||||
|   where |   where | ||||||
| @ -925,7 +955,7 @@ rawnumberp = label "number" $ do | |||||||
|   rawNumber <- fmap Right leadingDecimalPt <|> leadingDigits |   rawNumber <- fmap Right leadingDecimalPt <|> leadingDigits | ||||||
| 
 | 
 | ||||||
|   -- Guard against mistyped numbers |   -- Guard against mistyped numbers | ||||||
|   mExtraDecimalSep <- optional $ lookAhead $ satisfy isDecimalPointChar |   mExtraDecimalSep <- optional $ lookAhead $ satisfy isDecimalMark | ||||||
|   when (isJust mExtraDecimalSep) $ |   when (isJust mExtraDecimalSep) $ | ||||||
|     Fail.fail "invalid number (invalid use of separator)" |     Fail.fail "invalid number (invalid use of separator)" | ||||||
| 
 | 
 | ||||||
| @ -941,7 +971,7 @@ rawnumberp = label "number" $ do | |||||||
| 
 | 
 | ||||||
|   leadingDecimalPt :: TextParser m RawNumber |   leadingDecimalPt :: TextParser m RawNumber | ||||||
|   leadingDecimalPt = do |   leadingDecimalPt = do | ||||||
|     decPt <- satisfy isDecimalPointChar |     decPt <- satisfy isDecimalMark | ||||||
|     decGrp <- digitgroupp |     decGrp <- digitgroupp | ||||||
|     pure $ NoSeparators mempty (Just (decPt, decGrp)) |     pure $ NoSeparators mempty (Just (decPt, decGrp)) | ||||||
| 
 | 
 | ||||||
| @ -962,7 +992,7 @@ rawnumberp = label "number" $ do | |||||||
| 
 | 
 | ||||||
|   withDecimalPt :: Char -> [DigitGrp] -> TextParser m RawNumber |   withDecimalPt :: Char -> [DigitGrp] -> TextParser m RawNumber | ||||||
|   withDecimalPt digitSep digitGroups = do |   withDecimalPt digitSep digitGroups = do | ||||||
|     decPt <- satisfy $ \c -> isDecimalPointChar c && c /= digitSep |     decPt <- satisfy $ \c -> isDecimalMark c && c /= digitSep | ||||||
|     decDigitGrp <- option mempty digitgroupp |     decDigitGrp <- option mempty digitgroupp | ||||||
| 
 | 
 | ||||||
|     pure $ WithSeparators digitSep digitGroups (Just (decPt, decDigitGrp)) |     pure $ WithSeparators digitSep digitGroups (Just (decPt, decDigitGrp)) | ||||||
| @ -974,21 +1004,17 @@ rawnumberp = label "number" $ do | |||||||
|     -> [DigitGrp] |     -> [DigitGrp] | ||||||
|     -> Either AmbiguousNumber RawNumber |     -> Either AmbiguousNumber RawNumber | ||||||
|   withoutDecimalPt grp1 sep grp2 grps |   withoutDecimalPt grp1 sep grp2 grps | ||||||
|     | null grps && isDecimalPointChar sep = |     | null grps && isDecimalMark sep = | ||||||
|         Left $ AmbiguousNumber grp1 sep grp2 |         Left $ AmbiguousNumber grp1 sep grp2 | ||||||
|     | otherwise = Right $ WithSeparators sep (grp1:grp2:grps) Nothing |     | otherwise = Right $ WithSeparators sep (grp1:grp2:grps) Nothing | ||||||
| 
 | 
 | ||||||
|   trailingDecimalPt :: DigitGrp -> TextParser m RawNumber |   trailingDecimalPt :: DigitGrp -> TextParser m RawNumber | ||||||
|   trailingDecimalPt grp1 = do |   trailingDecimalPt grp1 = do | ||||||
|     decPt <- satisfy isDecimalPointChar |     decPt <- satisfy isDecimalMark | ||||||
|     pure $ NoSeparators grp1 (Just (decPt, mempty)) |     pure $ NoSeparators grp1 (Just (decPt, mempty)) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| isDecimalPointChar :: Char -> Bool |  | ||||||
| isDecimalPointChar c = c == '.' || c == ',' |  | ||||||
| 
 |  | ||||||
| isDigitSeparatorChar :: Char -> Bool | isDigitSeparatorChar :: Char -> Bool | ||||||
| isDigitSeparatorChar c = isDecimalPointChar c || c == ' ' | isDigitSeparatorChar c = isDecimalMark c || c == ' ' | ||||||
| 
 | 
 | ||||||
| -- | Some kinds of number literal we might parse. | -- | Some kinds of number literal we might parse. | ||||||
| data RawNumber | data RawNumber | ||||||
|  | |||||||
| @ -78,7 +78,7 @@ import Text.Printf (printf) | |||||||
| 
 | 
 | ||||||
| import Hledger.Data | import Hledger.Data | ||||||
| import Hledger.Utils | import Hledger.Utils | ||||||
| import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos, journalFinalise) | import Hledger.Read.Common ( Reader(..),InputOpts(..), amountp, statusp, genericSourcePos, journalFinalise ) | ||||||
| 
 | 
 | ||||||
| --- ** doctest setup | --- ** doctest setup | ||||||
| -- $setup | -- $setup | ||||||
| @ -364,7 +364,7 @@ Grammar for the CSV conversion rules, more or less: | |||||||
| 
 | 
 | ||||||
| RULES: RULE* | RULES: RULE* | ||||||
| 
 | 
 | ||||||
| RULE: ( FIELD-LIST | FIELD-ASSIGNMENT | CONDITIONAL-BLOCK | SKIP | NEWEST-FIRST | DATE-FORMAT | COMMENT | BLANK ) NEWLINE | RULE: ( FIELD-LIST | FIELD-ASSIGNMENT | CONDITIONAL-BLOCK | SKIP | NEWEST-FIRST | DATE-FORMAT | DECIMAL-MARK | COMMENT | BLANK ) NEWLINE | ||||||
| 
 | 
 | ||||||
| FIELD-LIST: fields SPACE FIELD-NAME ( SPACE? , SPACE? FIELD-NAME )* | FIELD-LIST: fields SPACE FIELD-NAME ( SPACE? , SPACE? FIELD-NAME )* | ||||||
| 
 | 
 | ||||||
| @ -462,6 +462,7 @@ directivep = (do | |||||||
| directives :: [String] | directives :: [String] | ||||||
| directives = | directives = | ||||||
|   ["date-format" |   ["date-format" | ||||||
|  |   ,"decimal-mark" | ||||||
|   ,"separator" |   ,"separator" | ||||||
|   -- ,"default-account" |   -- ,"default-account" | ||||||
|   -- ,"default-currency" |   -- ,"default-currency" | ||||||
| @ -1048,9 +1049,10 @@ getBalance rules record currency n = do | |||||||
| parseAmount :: CsvRules -> CsvRecord -> String -> String -> MixedAmount | parseAmount :: CsvRules -> CsvRecord -> String -> String -> MixedAmount | ||||||
| parseAmount rules record currency s = | parseAmount rules record currency s = | ||||||
|   either mkerror (Mixed . (:[])) $  -- PARTIAL: |   either mkerror (Mixed . (:[])) $  -- PARTIAL: | ||||||
|   runParser (evalStateT (amountp <* eof) nulljournal) "" $ |   runParser (evalStateT (amountp <* eof) journalparsestate) "" $ | ||||||
|   T.pack $ (currency++) $ simplifySign s |   T.pack $ (currency++) $ simplifySign s | ||||||
|   where |   where | ||||||
|  |     journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules} | ||||||
|     mkerror e = error' $ unlines |     mkerror e = error' $ unlines | ||||||
|       ["error: could not parse \""++s++"\" as an amount" |       ["error: could not parse \""++s++"\" as an amount" | ||||||
|       ,showRecord record |       ,showRecord record | ||||||
| @ -1062,7 +1064,8 @@ parseAmount rules record currency s = | |||||||
|         ++"or add or change your skip rule" |         ++"or add or change your skip rule" | ||||||
|       ] |       ] | ||||||
| 
 | 
 | ||||||
| -- XXX unify these | -- XXX unify these ^v | ||||||
|  | 
 | ||||||
| -- | Almost but not quite the same as parseAmount. | -- | Almost but not quite the same as parseAmount. | ||||||
| -- Given a non-empty amount string (from CSV) to parse, along with a | -- Given a non-empty amount string (from CSV) to parse, along with a | ||||||
| -- possibly non-empty currency symbol to prepend, | -- possibly non-empty currency symbol to prepend, | ||||||
| @ -1071,10 +1074,11 @@ parseAmount rules record currency s = | |||||||
| parseBalanceAmount :: CsvRules -> CsvRecord -> String -> Int -> String -> Amount | parseBalanceAmount :: CsvRules -> CsvRecord -> String -> Int -> String -> Amount | ||||||
| parseBalanceAmount rules record currency n s = | parseBalanceAmount rules record currency n s = | ||||||
|   either (mkerror n s) id $ |   either (mkerror n s) id $ | ||||||
|     runParser (evalStateT (amountp <* eof) nulljournal) "" $ |     runParser (evalStateT (amountp <* eof) journalparsestate) "" $ | ||||||
|     T.pack $ (currency++) $ simplifySign s |     T.pack $ (currency++) $ simplifySign s | ||||||
|                   -- the csv record's line number would be good |                   -- the csv record's line number would be good | ||||||
|   where |   where | ||||||
|  |     journalparsestate = nulljournal{jparsedecimalmark=parseDecimalMark rules} | ||||||
|     mkerror n s e = error' $ unlines |     mkerror n s e = error' $ unlines | ||||||
|       ["error: could not parse \""++s++"\" as balance"++show n++" amount" |       ["error: could not parse \""++s++"\" as balance"++show n++" amount" | ||||||
|       ,showRecord record |       ,showRecord record | ||||||
| @ -1083,6 +1087,15 @@ parseBalanceAmount rules record currency n s = | |||||||
|       ,"the parse error is:      "++customErrorBundlePretty e |       ,"the parse error is:      "++customErrorBundlePretty e | ||||||
|       ] |       ] | ||||||
| 
 | 
 | ||||||
|  | -- Read a valid decimal mark from the decimal-mark rule, if any. | ||||||
|  | -- If the rule is present with an invalid argument, raise an error. | ||||||
|  | parseDecimalMark :: CsvRules -> Maybe DecimalMark | ||||||
|  | parseDecimalMark rules = | ||||||
|  |   case rules `csvRule` "decimal-mark" of | ||||||
|  |     Nothing -> Nothing | ||||||
|  |     Just [c] | isDecimalMark c -> Just c | ||||||
|  |     Just s -> error' $ "decimal-mark's argument should be \".\" or \",\" (not \""++s++"\")" | ||||||
|  | 
 | ||||||
| -- | Make a balance assertion for the given amount, with the given parse | -- | Make a balance assertion for the given amount, with the given parse | ||||||
| -- position (to be shown in assertion failures), with the assertion type | -- position (to be shown in assertion failures), with the assertion type | ||||||
| -- possibly set by a balance-type rule. | -- possibly set by a balance-type rule. | ||||||
|  | |||||||
| @ -45,7 +45,8 @@ these are described more fully below, after the examples: | |||||||
| | [**`if` block**](#if-block)               | apply some rules to CSV records matched by patterns     | | | [**`if` block**](#if-block)               | apply some rules to CSV records matched by patterns     | | ||||||
| | [**`if` table**](#if-table)               | apply some rules to CSV records matched by patterns, alternate syntax | | | [**`if` table**](#if-table)               | apply some rules to CSV records matched by patterns, alternate syntax | | ||||||
| | [**`end`**](#end)                         | skip the remaining CSV records                          | | | [**`end`**](#end)                         | skip the remaining CSV records                          | | ||||||
| | [**`date-format`**](#date-format)         | describe the format of CSV dates                        | | | [**`date-format`**](#date-format)         | how to parse dates in CSV records                       | | ||||||
|  | | [**`decimal-mark`**](#decimal-mark)       | the decimal mark used in CSV amounts, if ambiguous      | | ||||||
| | [**`newest-first`**](#newest-first)       | disambiguate record order when there's only one date    | | | [**`newest-first`**](#newest-first)       | disambiguate record order when there's only one date    | | ||||||
| | [**`include`**](#include)                 | inline another CSV rules file                           | | | [**`include`**](#include)                 | inline another CSV rules file                           | | ||||||
| | [**`balance-type`**](#balance-type)       | choose which type of balance assignments to use         | | | [**`balance-type`**](#balance-type)       | choose which type of balance assignments to use         | | ||||||
| @ -716,6 +717,21 @@ For the supported strptime syntax, see:\ | |||||||
| <https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:formatTime> | <https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:formatTime> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | ## `decimal-mark` | ||||||
|  | 
 | ||||||
|  | ```rules | ||||||
|  | decimal-mark . | ||||||
|  | ``` | ||||||
|  | or: | ||||||
|  | ```rules | ||||||
|  | decimal-mark , | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | hledger automatically accepts either period or comma as a decimal mark when parsing numbers | ||||||
|  | (cf [Amounts](journal.html#amounts)). | ||||||
|  | However if any numbers in the CSV contain digit group marks, such as thousand-separating commas, | ||||||
|  | you should declare the decimal mark explicitly with this rule, to avoid misparsed numbers. | ||||||
|  | 
 | ||||||
| ## `newest-first` | ## `newest-first` | ||||||
| 
 | 
 | ||||||
| hledger always sorts the generated transactions by date. | hledger always sorts the generated transactions by date. | ||||||
|  | |||||||
| @ -906,6 +906,46 @@ $  ./csvtest.sh | |||||||
| 
 | 
 | ||||||
| >=0 | >=0 | ||||||
| 
 | 
 | ||||||
|  | # 45. decimal-mark helps parse ambiguous decimals correctly | ||||||
|  | < | ||||||
|  | 2020-01-01,"1,000" | ||||||
|  | 2020-01-02,"1.000" | ||||||
|  | 
 | ||||||
|  | RULES | ||||||
|  | fields date,amount | ||||||
|  | decimal-mark . | ||||||
|  | 
 | ||||||
|  | $  ./csvtest.sh | ||||||
|  | 2020-01-01 | ||||||
|  |     expenses:unknown       1,000.000 | ||||||
|  |     income:unknown        -1,000.000 | ||||||
|  | 
 | ||||||
|  | 2020-01-02 | ||||||
|  |     expenses:unknown           1.000 | ||||||
|  |     income:unknown            -1.000 | ||||||
|  | 
 | ||||||
|  | >= | ||||||
|  | 
 | ||||||
|  | # 46.  | ||||||
|  | < | ||||||
|  | 2020-01-01,"1,000" | ||||||
|  | 2020-01-02,"1.000" | ||||||
|  | 
 | ||||||
|  | RULES | ||||||
|  | fields date,amount | ||||||
|  | decimal-mark , | ||||||
|  | 
 | ||||||
|  | $  ./csvtest.sh | ||||||
|  | 2020-01-01 | ||||||
|  |     expenses:unknown           1,000 | ||||||
|  |     income:unknown            -1,000 | ||||||
|  | 
 | ||||||
|  | 2020-01-02 | ||||||
|  |     expenses:unknown       1.000,000 | ||||||
|  |     income:unknown        -1.000,000 | ||||||
|  | 
 | ||||||
|  | >= | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| ## .  | ## .  | ||||||
| #< | #< | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user