packages feed

hledger-lib 1.24.99 → 1.25

raw patch · 25 files changed

+831/−478 lines, 25 filesdep ~aesonPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson

API changes (from Hackage documentation)

- Hledger.Data: Last :: SmartSequence
- Hledger.Data: Next :: SmartSequence
- Hledger.Data: This :: SmartSequence
- Hledger.Data: data SmartSequence
- Hledger.Data.Dates: maybePeriod :: Day -> Text -> Maybe (Interval, DateSpan)
- Hledger.Data.Posting: accountNameApplyAliases :: [AccountAlias] -> AccountName -> Either RegexError AccountName
- Hledger.Data.Posting: accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> Either RegexError AccountName
- Hledger.Data.Posting: accountNamePostingType :: AccountName -> PostingType
- Hledger.Data.Posting: accountNameWithPostingType :: PostingType -> AccountName -> AccountName
- Hledger.Data.Posting: accountNameWithoutPostingType :: AccountName -> AccountName
- Hledger.Data.Posting: concatAccountNames :: [AccountName] -> AccountName
- Hledger.Data.Posting: joinAccountNames :: AccountName -> AccountName -> AccountName
- Hledger.Data.Types: Last :: SmartSequence
- Hledger.Data.Types: Next :: SmartSequence
- Hledger.Data.Types: This :: SmartSequence
- Hledger.Data.Types: data SmartSequence
- Hledger.Data.Types: instance GHC.Show.Show Hledger.Data.Types.SmartSequence
- Hledger.Data.Valuation: Cost :: Costing
- Hledger.Data.Valuation: NoCost :: Costing
- Hledger.Data.Valuation: data Costing
- Hledger.Data.Valuation: instance GHC.Classes.Eq Hledger.Data.Valuation.Costing
- Hledger.Data.Valuation: instance GHC.Show.Show Hledger.Data.Valuation.Costing
- Hledger.Reports.ReportOptions: [cost_] :: ReportOpts -> Costing
- Hledger.Reports.ReportOptions: cost :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Costing
+ Hledger.Data: Conversion :: AccountType
+ Hledger.Data: [jaccounttypes] :: Journal -> Map AccountName AccountType
+ Hledger.Data: [jdeclaredaccounttags] :: Journal -> Map AccountName [Tag]
+ Hledger.Data: isAccountSubtypeOf :: AccountType -> AccountType -> Bool
+ Hledger.Data.AccountName: accountNameApplyAliases :: [AccountAlias] -> AccountName -> Either RegexError AccountName
+ Hledger.Data.AccountName: accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> Either RegexError AccountName
+ Hledger.Data.AccountName: accountNameInferType :: AccountName -> Maybe AccountType
+ Hledger.Data.AccountName: accountNamePostingType :: AccountName -> PostingType
+ Hledger.Data.AccountName: accountNameType :: Map AccountName AccountType -> AccountName -> Maybe AccountType
+ Hledger.Data.AccountName: accountNameWithPostingType :: PostingType -> AccountName -> AccountName
+ Hledger.Data.AccountName: accountNameWithoutPostingType :: AccountName -> AccountName
+ Hledger.Data.AccountName: assetAccountRegex :: Regexp
+ Hledger.Data.AccountName: cashAccountRegex :: Regexp
+ Hledger.Data.AccountName: concatAccountNames :: [AccountName] -> AccountName
+ Hledger.Data.AccountName: conversionAccountRegex :: Regexp
+ Hledger.Data.AccountName: equityAccountRegex :: Regexp
+ Hledger.Data.AccountName: expenseAccountRegex :: Regexp
+ Hledger.Data.AccountName: joinAccountNames :: AccountName -> AccountName -> AccountName
+ Hledger.Data.AccountName: liabilityAccountRegex :: Regexp
+ Hledger.Data.AccountName: revenueAccountRegex :: Regexp
+ Hledger.Data.Amount: showAmountPrice :: Amount -> WideBuilder
+ Hledger.Data.Dates: spansFromBoundaries :: Day -> [Day] -> [DateSpan]
+ Hledger.Data.Journal: journalAccountNameTree :: Journal -> Tree AccountName
+ Hledger.Data.Journal: journalAccountTags :: Journal -> AccountName -> [Tag]
+ Hledger.Data.Journal: journalAccountType :: Journal -> AccountName -> Maybe AccountType
+ Hledger.Data.Journal: journalAccountTypes :: Journal -> Map AccountName AccountType
+ Hledger.Data.Journal: journalAddAccountTypes :: Journal -> Journal
+ Hledger.Data.Journal: journalAddInferredEquityPostings :: Journal -> Journal
+ Hledger.Data.Journal: journalConversionAccount :: Journal -> AccountName
+ Hledger.Data.Journal: journalInheritedAccountTags :: Journal -> AccountName -> [Tag]
+ Hledger.Data.Journal: journalLeafAccountNames :: Journal -> [AccountName]
+ Hledger.Data.Journal: journalPostingsAddAccountTags :: Journal -> Journal
+ Hledger.Data.Posting: postingAddInferredEquityPostings :: Text -> Posting -> [Posting]
+ Hledger.Data.Posting: postingAddTags :: Posting -> [Tag] -> Posting
+ Hledger.Data.Transaction: transactionAddInferredEquityPostings :: AccountName -> Transaction -> Transaction
+ Hledger.Data.Types: Conversion :: AccountType
+ Hledger.Data.Types: [jaccounttypes] :: Journal -> Map AccountName AccountType
+ Hledger.Data.Types: [jdeclaredaccounttags] :: Journal -> Map AccountName [Tag]
+ Hledger.Data.Types: isAccountSubtypeOf :: AccountType -> AccountType -> Bool
+ Hledger.Data.Valuation: NoConversionOp :: ConversionOp
+ Hledger.Data.Valuation: ToCost :: ConversionOp
+ Hledger.Data.Valuation: data ConversionOp
+ Hledger.Data.Valuation: instance GHC.Classes.Eq Hledger.Data.Valuation.ConversionOp
+ Hledger.Data.Valuation: instance GHC.Show.Show Hledger.Data.Valuation.ConversionOp
+ Hledger.Query: Type :: [AccountType] -> Query
+ Hledger.Query: matchesAccountExtra :: (AccountName -> Maybe AccountType) -> (AccountName -> [Tag]) -> Query -> AccountName -> Bool
+ Hledger.Query: matchesPostingExtra :: (AccountName -> Maybe AccountType) -> Query -> Posting -> Bool
+ Hledger.Query: matchesTransactionExtra :: (AccountName -> Maybe AccountType) -> Query -> Transaction -> Bool
+ Hledger.Query: parseAccountType :: Bool -> Text -> Either String AccountType
+ Hledger.Query: queryIsTag :: Query -> Bool
+ Hledger.Query: queryIsType :: Query -> Bool
+ Hledger.Read.Common: [infer_equity_] :: InputOpts -> Bool
+ Hledger.Read.Common: addDeclaredAccountTags :: AccountName -> [Tag] -> JournalParser m ()
+ Hledger.Read.Common: infer_equity :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
+ Hledger.Read.InputOptions: [infer_equity_] :: InputOpts -> Bool
+ Hledger.Read.InputOptions: infer_equity :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
+ Hledger.Reports.ReportOptions: [conversionop_] :: ReportOpts -> Maybe ConversionOp
+ Hledger.Reports.ReportOptions: conversionop :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n (Maybe ConversionOp)
+ Hledger.Reports.ReportOptions: setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec
- Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountType [AccountName] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
+ Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Data: SmartRelative :: SmartSequence -> SmartInterval -> SmartDate
+ Hledger.Data: SmartRelative :: Integer -> SmartInterval -> SmartDate
- Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a2Yv8 => Lens' c_a2Yv8 BalancingOpts
+ Hledger.Data.Balancing: balancingOpts :: HasBalancingOpts c_a33DV => Lens' c_a33DV BalancingOpts
- Hledger.Data.Balancing: class HasBalancingOpts c_a2Yv8
+ Hledger.Data.Balancing: class HasBalancingOpts c_a33DV
- Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a2Yv8 => Lens' c_a2Yv8 (Maybe (Map CommoditySymbol AmountStyle))
+ Hledger.Data.Balancing: commodity_styles :: HasBalancingOpts c_a33DV => Lens' c_a33DV (Maybe (Map CommoditySymbol AmountStyle))
- Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a2Yv8 => Lens' c_a2Yv8 Bool
+ Hledger.Data.Balancing: ignore_assertions :: HasBalancingOpts c_a33DV => Lens' c_a33DV Bool
- Hledger.Data.Balancing: infer_transaction_prices :: HasBalancingOpts c_a2Yv8 => Lens' c_a2Yv8 Bool
+ Hledger.Data.Balancing: infer_transaction_prices :: HasBalancingOpts c_a33DV => Lens' c_a33DV Bool
- Hledger.Data.Journal: journalToCost :: Journal -> Journal
+ Hledger.Data.Journal: journalToCost :: ConversionOp -> Journal -> Journal
- Hledger.Data.Posting: postingToCost :: Map CommoditySymbol AmountStyle -> Posting -> Posting
+ Hledger.Data.Posting: postingToCost :: Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Posting
- Hledger.Data.Transaction: transactionToCost :: Map CommoditySymbol AmountStyle -> Transaction -> Transaction
+ Hledger.Data.Transaction: transactionToCost :: Map CommoditySymbol AmountStyle -> ConversionOp -> Transaction -> Transaction
- Hledger.Data.TransactionModifier: modifyTransactions :: Map CommoditySymbol AmountStyle -> Day -> [TransactionModifier] -> [Transaction] -> Either String [Transaction]
+ Hledger.Data.TransactionModifier: modifyTransactions :: (AccountName -> Maybe AccountType) -> (AccountName -> [Tag]) -> Map CommoditySymbol AmountStyle -> Day -> [TransactionModifier] -> [Transaction] -> Either String [Transaction]
- Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountType [AccountName] -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
+ Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Data.Types: SmartRelative :: SmartSequence -> SmartInterval -> SmartDate
+ Hledger.Data.Types: SmartRelative :: Integer -> SmartInterval -> SmartDate
- Hledger.Data.Valuation: mixedAmountToCost :: Costing -> Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount
+ Hledger.Data.Valuation: mixedAmountToCost :: Map CommoditySymbol AmountStyle -> ConversionOp -> MixedAmount -> MixedAmount
- Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
+ Hledger.Read.Common: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
- Hledger.Read.Common: aliases :: HasInputOpts c_a3bHU => Lens' c_a3bHU [String]
+ Hledger.Read.Common: aliases :: HasInputOpts c_a3gOc => Lens' c_a3gOc [String]
- Hledger.Read.Common: anon__ :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.Common: anon__ :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.Common: auto__ :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.Common: auto__ :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.Common: balancingopts :: HasInputOpts c_a3bHU => Lens' c_a3bHU BalancingOpts
+ Hledger.Read.Common: balancingopts :: HasInputOpts c_a3gOc => Lens' c_a3gOc BalancingOpts
- Hledger.Read.Common: class HasInputOpts c_a3bHU
+ Hledger.Read.Common: class HasInputOpts c_a3gOc
- Hledger.Read.Common: forecast :: HasInputOpts c_a3bHU => Lens' c_a3bHU (Maybe DateSpan)
+ Hledger.Read.Common: forecast :: HasInputOpts c_a3gOc => Lens' c_a3gOc (Maybe DateSpan)
- Hledger.Read.Common: inputOpts :: HasInputOpts c_a3bHU => Lens' c_a3bHU InputOpts
+ Hledger.Read.Common: inputOpts :: HasInputOpts c_a3gOc => Lens' c_a3gOc InputOpts
- Hledger.Read.Common: ioDay :: HasInputOpts c_a3bHU => Lens' c_a3bHU Day
+ Hledger.Read.Common: ioDay :: HasInputOpts c_a3gOc => Lens' c_a3gOc Day
- Hledger.Read.Common: mformat :: HasInputOpts c_a3bHU => Lens' c_a3bHU (Maybe StorageFormat)
+ Hledger.Read.Common: mformat :: HasInputOpts c_a3gOc => Lens' c_a3gOc (Maybe StorageFormat)
- Hledger.Read.Common: mrules_file :: HasInputOpts c_a3bHU => Lens' c_a3bHU (Maybe FilePath)
+ Hledger.Read.Common: mrules_file :: HasInputOpts c_a3gOc => Lens' c_a3gOc (Maybe FilePath)
- Hledger.Read.Common: new__ :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.Common: new__ :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.Common: new_save :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.Common: new_save :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.Common: pivot :: HasInputOpts c_a3bHU => Lens' c_a3bHU String
+ Hledger.Read.Common: pivot :: HasInputOpts c_a3gOc => Lens' c_a3gOc String
- Hledger.Read.Common: reportspan :: HasInputOpts c_a3bHU => Lens' c_a3bHU DateSpan
+ Hledger.Read.Common: reportspan :: HasInputOpts c_a3gOc => Lens' c_a3gOc DateSpan
- Hledger.Read.Common: strict :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.Common: strict :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.InputOptions: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
+ Hledger.Read.InputOptions: InputOpts :: Maybe StorageFormat -> Maybe FilePath -> [String] -> Bool -> Bool -> Bool -> String -> Maybe DateSpan -> DateSpan -> Bool -> Bool -> BalancingOpts -> Bool -> Day -> InputOpts
- Hledger.Read.InputOptions: aliases :: HasInputOpts c_a3bHU => Lens' c_a3bHU [String]
+ Hledger.Read.InputOptions: aliases :: HasInputOpts c_a3gOc => Lens' c_a3gOc [String]
- Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.InputOptions: anon__ :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.InputOptions: auto__ :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a3bHU => Lens' c_a3bHU BalancingOpts
+ Hledger.Read.InputOptions: balancingopts :: HasInputOpts c_a3gOc => Lens' c_a3gOc BalancingOpts
- Hledger.Read.InputOptions: class HasInputOpts c_a3bHU
+ Hledger.Read.InputOptions: class HasInputOpts c_a3gOc
- Hledger.Read.InputOptions: forecast :: HasInputOpts c_a3bHU => Lens' c_a3bHU (Maybe DateSpan)
+ Hledger.Read.InputOptions: forecast :: HasInputOpts c_a3gOc => Lens' c_a3gOc (Maybe DateSpan)
- Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a3bHU => Lens' c_a3bHU InputOpts
+ Hledger.Read.InputOptions: inputOpts :: HasInputOpts c_a3gOc => Lens' c_a3gOc InputOpts
- Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a3bHU => Lens' c_a3bHU Day
+ Hledger.Read.InputOptions: ioDay :: HasInputOpts c_a3gOc => Lens' c_a3gOc Day
- Hledger.Read.InputOptions: mformat :: HasInputOpts c_a3bHU => Lens' c_a3bHU (Maybe StorageFormat)
+ Hledger.Read.InputOptions: mformat :: HasInputOpts c_a3gOc => Lens' c_a3gOc (Maybe StorageFormat)
- Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a3bHU => Lens' c_a3bHU (Maybe FilePath)
+ Hledger.Read.InputOptions: mrules_file :: HasInputOpts c_a3gOc => Lens' c_a3gOc (Maybe FilePath)
- Hledger.Read.InputOptions: new__ :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.InputOptions: new__ :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.InputOptions: new_save :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.InputOptions: new_save :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Read.InputOptions: pivot :: HasInputOpts c_a3bHU => Lens' c_a3bHU String
+ Hledger.Read.InputOptions: pivot :: HasInputOpts c_a3gOc => Lens' c_a3gOc String
- Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a3bHU => Lens' c_a3bHU DateSpan
+ Hledger.Read.InputOptions: reportspan :: HasInputOpts c_a3gOc => Lens' c_a3gOc DateSpan
- Hledger.Read.InputOptions: strict :: HasInputOpts c_a3bHU => Lens' c_a3bHU Bool
+ Hledger.Read.InputOptions: strict :: HasInputOpts c_a3gOc => Lens' c_a3gOc Bool
- Hledger.Reports.MultiBalanceReport: generateMultiBalanceReport :: ReportSpec -> Journal -> PriceOracle -> [(DateSpan, [Posting])] -> HashMap AccountName Account -> MultiBalanceReport
+ Hledger.Reports.MultiBalanceReport: generateMultiBalanceReport :: ReportSpec -> Journal -> PriceOracle -> Set AccountName -> [(DateSpan, [Posting])] -> HashMap AccountName Account -> MultiBalanceReport
- Hledger.Reports.MultiBalanceReport: getPostingsByColumn :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> [(DateSpan, [Posting])]
+ Hledger.Reports.MultiBalanceReport: getPostingsByColumn :: ReportSpec -> Journal -> PriceOracle -> [DateSpan] -> [(DateSpan, [Posting])]
- Hledger.Reports.MultiBalanceReport: multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> MultiBalanceReport
+ Hledger.Reports.MultiBalanceReport: multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> Set AccountName -> MultiBalanceReport
- Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Costing -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> Bool -> [Text] -> Bool -> Bool -> Bool -> BalanceCalculation -> BalanceAccumulation -> Maybe Text -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Bool -> Layout -> ReportOpts
+ Hledger.Reports.ReportOptions: ReportOpts :: Period -> Interval -> [Status] -> Maybe ConversionOp -> Maybe ValuationType -> Bool -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> StringFormat -> Bool -> [Text] -> Bool -> Bool -> Bool -> BalanceCalculation -> BalanceAccumulation -> Maybe Text -> AccountListMode -> Int -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe NormalSign -> Bool -> Bool -> Layout -> ReportOpts
- Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz AccountListMode
+ Hledger.Reports.ReportOptions: accountlistmode :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n AccountListMode
- Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: average :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz BalanceAccumulation
+ Hledger.Reports.ReportOptions: balanceaccum :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n BalanceAccumulation
- Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz BalanceCalculation
+ Hledger.Reports.ReportOptions: balancecalc :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n BalanceCalculation
- Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz (Maybe Text)
+ Hledger.Reports.ReportOptions: budgetpat :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n (Maybe Text)
- Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a3iQz
+ Hledger.Reports.ReportOptions: class HasReportOptsNoUpdate c_a3o9n
- Hledger.Reports.ReportOptions: class HasReportSpec c_a3jxW
+ Hledger.Reports.ReportOptions: class HasReportSpec c_a3oQK
- Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: color__ :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: date2NoUpdate :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: declared :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz (Maybe Int)
+ Hledger.Reports.ReportOptions: depthNoUpdate :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n (Maybe Int)
- Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Int
+ Hledger.Reports.ReportOptions: drop__ :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Int
- Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: empty__ :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz StringFormat
+ Hledger.Reports.ReportOptions: format :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n StringFormat
- Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: infer_prices :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Interval
+ Hledger.Reports.ReportOptions: interval :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Interval
- Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: invert :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Layout
+ Hledger.Reports.ReportOptions: layout :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Layout
- Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: no_elide :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: no_total :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz (Maybe NormalSign)
+ Hledger.Reports.ReportOptions: normalbalance :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n (Maybe NormalSign)
- Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: percent :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Period
+ Hledger.Reports.ReportOptions: periodNoUpdate :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Period
- Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: pretty :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz [Text]
+ Hledger.Reports.ReportOptions: querystringNoUpdate :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n [Text]
- Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: realNoUpdate :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: related :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz ReportOpts
+ Hledger.Reports.ReportOptions: reportOptsNoUpdate :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n ReportOpts
- Hledger.Reports.ReportOptions: reportSpan :: Journal -> ReportSpec -> DateSpan
+ Hledger.Reports.ReportOptions: reportSpan :: Journal -> ReportSpec -> (DateSpan, [DateSpan])
- Hledger.Reports.ReportOptions: reportSpanBothDates :: Journal -> ReportSpec -> DateSpan
+ Hledger.Reports.ReportOptions: reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, [DateSpan])
- Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a3jxW => Lens' c_a3jxW ReportSpec
+ Hledger.Reports.ReportOptions: reportSpec :: HasReportSpec c_a3oQK => Lens' c_a3oQK ReportSpec
- Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: row_total :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a3jxW => Lens' c_a3jxW Day
+ Hledger.Reports.ReportOptions: rsDay :: HasReportSpec c_a3oQK => Lens' c_a3oQK Day
- Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a3jxW => Lens' c_a3jxW Query
+ Hledger.Reports.ReportOptions: rsQuery :: HasReportSpec c_a3oQK => Lens' c_a3oQK Query
- Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a3jxW => Lens' c_a3jxW [QueryOpt]
+ Hledger.Reports.ReportOptions: rsQueryOpts :: HasReportSpec c_a3oQK => Lens' c_a3oQK [QueryOpt]
- Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a3jxW => Lens' c_a3jxW ReportOpts
+ Hledger.Reports.ReportOptions: rsReportOpts :: HasReportSpec c_a3oQK => Lens' c_a3oQK ReportOpts
- Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: show_costs :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: sort_amount :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz [Status]
+ Hledger.Reports.ReportOptions: statusesNoUpdate :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n [Status]
- Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: transpose__ :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz Bool
+ Hledger.Reports.ReportOptions: txn_dates :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n Bool
- Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a3iQz => Lens' c_a3iQz (Maybe ValuationType)
+ Hledger.Reports.ReportOptions: value :: HasReportOptsNoUpdate c_a3o9n => Lens' c_a3o9n (Maybe ValuationType)

Files

CHANGES.md view
@@ -9,14 +9,29 @@ Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog. -# a98e6125f+# 1.25 2022-03-04 -Improvements+- hledger-lib now builds with GHC 9.2 and latest deps. +  ([#1774](https://github.com/simonmichael/hledger/issues/1774) -- hledger-lib now builds with GHC 9.2 and newer libs (#1774).+- Journal has a new jaccounttypes map.+  The journalAccountType lookup function makes it easy to check an account's type.+  The journalTags and journalInheritedTags functions look up an account's tags.+  Functions like journalFilterPostings and journalFilterTransactions,+  and new matching functions matchesAccountExtra, matchesPostingExtra+  and matchesTransactionExtra, use these to allow more powerful matching+  that is aware of account types and tags. +- Journal has a new jdeclaredaccounttags field+  for easy lookup of account tags.+  Query.matchesTaggedAccount is a tag-aware version of matchesAccount.++- Some account name functions have moved from Hledger.Data.Posting+  to Hledger.Data.AccountName:+  accountNamePostingType, accountNameWithPostingType, accountNameWithoutPostingType,+  joinAccountNames, concatAccountNames, accountNameApplyAliases, accountNameApplyAliasesMemo.+ - Renamed: CommodityLayout to Layout.-  (Stephen Morgan)  # 1.24.1 2021-12-10 
Hledger/Data/AccountName.hs view
@@ -20,6 +20,15 @@   ,accountNameToAccountRegexCI   ,accountNameTreeFrom   ,accountSummarisedName+  ,accountNameInferType+  ,accountNameType+  ,assetAccountRegex+  ,cashAccountRegex+  ,liabilityAccountRegex+  ,equityAccountRegex+  ,conversionAccountRegex+  ,revenueAccountRegex+  ,expenseAccountRegex   ,acctsep   ,acctsepchar   ,clipAccountName@@ -36,16 +45,28 @@   ,subAccountNamesFrom   ,topAccountNames   ,unbudgetedAccountName+  ,accountNamePostingType+  ,accountNameWithoutPostingType+  ,accountNameWithPostingType+  ,joinAccountNames+  ,concatAccountNames+  ,accountNameApplyAliases+  ,accountNameApplyAliasesMemo   ,tests_AccountName ) where -import Data.Foldable (toList)+import Control.Applicative ((<|>))+import Control.Monad (foldM)+import Data.Foldable (asum, toList) import qualified Data.List.NonEmpty as NE+import qualified Data.Map as M+import Data.MemoUgly (memo) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Tree (Tree(..))+import Safe import Text.DocLayout (realLength)  import Hledger.Data.Types@@ -82,6 +103,36 @@       cs = accountNameComponents a       a' = accountLeafName a +-- | Regular expressions matching common English top-level account names,+-- used as a fallback when account types are not declared.+assetAccountRegex      = toRegexCI' "^assets?(:|$)"+cashAccountRegex       = toRegexCI' "^assets?(:.+)?:(cash|bank|che(ck|que?)(ing)?|savings?|current)(:|$)"+liabilityAccountRegex  = toRegexCI' "^(debts?|liabilit(y|ies))(:|$)"+equityAccountRegex     = toRegexCI' "^equity(:|$)"+conversionAccountRegex = toRegexCI' "^equity:(trad(e|ing)|conversion)s?(:|$)"+revenueAccountRegex    = toRegexCI' "^(income|revenue)s?(:|$)"+expenseAccountRegex    = toRegexCI' "^expenses?(:|$)"++-- | Try to guess an account's type from its name,+-- matching common English top-level account names.+accountNameInferType :: AccountName -> Maybe AccountType+accountNameInferType a+  | regexMatchText cashAccountRegex       a = Just Cash+  | regexMatchText assetAccountRegex      a = Just Asset+  | regexMatchText liabilityAccountRegex  a = Just Liability+  | regexMatchText conversionAccountRegex a = Just Conversion+  | regexMatchText equityAccountRegex     a = Just Equity+  | regexMatchText revenueAccountRegex    a = Just Revenue+  | regexMatchText expenseAccountRegex    a = Just Expense+  | otherwise                               = Nothing++-- Extract the 'AccountType' of an 'AccountName' by looking it up in the+-- provided Map, traversing the parent accounts if necessary. If none of those+-- work, try 'accountNameInferType'.+accountNameType :: M.Map AccountName AccountType -> AccountName -> Maybe AccountType+accountNameType atypes a = asum (map (`M.lookup` atypes) $ a : parentAccountNames a)+                         <|> accountNameInferType a+ accountNameLevel :: AccountName -> Int accountNameLevel "" = 0 accountNameLevel a = T.length (T.filter (==acctsepchar) a) + 1@@ -91,6 +142,65 @@ unbudgetedAccountName :: T.Text unbudgetedAccountName = "<unbudgeted>" +accountNamePostingType :: AccountName -> PostingType+accountNamePostingType a+    | T.null a = RegularPosting+    | T.head a == '[' && T.last a == ']' = BalancedVirtualPosting+    | T.head a == '(' && T.last a == ')' = VirtualPosting+    | otherwise = RegularPosting++accountNameWithoutPostingType :: AccountName -> AccountName+accountNameWithoutPostingType a = case accountNamePostingType a of+                                    BalancedVirtualPosting -> textUnbracket a+                                    VirtualPosting -> textUnbracket a+                                    RegularPosting -> a++accountNameWithPostingType :: PostingType -> AccountName -> AccountName+accountNameWithPostingType BalancedVirtualPosting = wrap "[" "]" . accountNameWithoutPostingType+accountNameWithPostingType VirtualPosting         = wrap "(" ")" . accountNameWithoutPostingType+accountNameWithPostingType RegularPosting         = accountNameWithoutPostingType++-- | Prefix one account name to another, preserving posting type+-- indicators like concatAccountNames.+joinAccountNames :: AccountName -> AccountName -> AccountName+joinAccountNames a b = concatAccountNames $ filter (not . T.null) [a,b]++-- | Join account names into one. If any of them has () or [] posting type+-- indicators, these (the first type encountered) will also be applied to+-- the resulting account name.+concatAccountNames :: [AccountName] -> AccountName+concatAccountNames as = accountNameWithPostingType t $ T.intercalate ":" $ map accountNameWithoutPostingType as+    where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as++-- | Rewrite an account name using all matching aliases from the given list, in sequence.+-- Each alias sees the result of applying the previous aliases.+-- Or, return any error arising from a bad regular expression in the aliases.+accountNameApplyAliases :: [AccountAlias] -> AccountName -> Either RegexError AccountName+accountNameApplyAliases aliases a =+  let (aname,atype) = (accountNameWithoutPostingType a, accountNamePostingType a)+  in foldM+     (\acct alias -> dbg6 "result" $ aliasReplace (dbg6 "alias" alias) (dbg6 "account" acct))+     aname+     aliases+     >>= Right . accountNameWithPostingType atype++-- | Memoising version of accountNameApplyAliases, maybe overkill.+accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> Either RegexError AccountName+accountNameApplyAliasesMemo aliases = memo (accountNameApplyAliases aliases)+  -- XXX re-test this memoisation++-- aliasMatches :: AccountAlias -> AccountName -> Bool+-- aliasMatches (BasicAlias old _) a = old `isAccountNamePrefixOf` a+-- aliasMatches (RegexAlias re  _) a = regexMatchesCI re a++aliasReplace :: AccountAlias -> AccountName -> Either RegexError AccountName+aliasReplace (BasicAlias old new) a+  | old `isAccountNamePrefixOf` a || old == a =+      Right $ new <> T.drop (T.length old) a+  | otherwise = Right a+aliasReplace (RegexAlias re repl) a =+  fmap T.pack . regexReplace re repl $ T.unpack a -- XXX+ -- | Remove some number of account name components from the front of the account name. -- If the special "<unbudgeted>" top-level account is present, it is preserved and -- dropping affects the rest of the account name.@@ -255,5 +365,16 @@     "assets:bank" `isSubAccountNameOf` "assets" @?= True     "assets:bank:checking" `isSubAccountNameOf` "assets" @?= False     "assets:bank" `isSubAccountNameOf` "my assets" @?= False+  ,testCase "accountNameInferType" $ do+    accountNameInferType "assets"            @?= Just Asset+    accountNameInferType "assets:cash"       @?= Just Cash+    accountNameInferType "assets:A/R"        @?= Just Asset+    accountNameInferType "liabilities"       @?= Just Liability+    accountNameInferType "equity"            @?= Just Equity+    accountNameInferType "equity:conversion" @?= Just Conversion+    accountNameInferType "expenses"          @?= Just Expense+    accountNameInferType "revenues"          @?= Just Revenue+    accountNameInferType "revenue"           @?= Just Revenue+    accountNameInferType "income"            @?= Just Revenue  ] 
Hledger/Data/Amount.hs view
@@ -80,6 +80,7 @@   amountUnstyled,   showAmountB,   showAmount,+  showAmountPrice,   cshowAmount,   showAmountWithZeroCommodity,   showAmountDebug,
Hledger/Data/Balancing.hs view
@@ -350,7 +350,7 @@ data BalancingState s = BalancingState {    -- read only    bsStyles       :: Maybe (M.Map CommoditySymbol AmountStyle)  -- ^ commodity display styles-  ,bsUnassignable :: S.Set AccountName                          -- ^ accounts in which balance assignments may not be used+  ,bsUnassignable :: S.Set AccountName                          -- ^ accounts where balance assignments may not be used (because of auto posting rules)   ,bsAssrt        :: Bool                                       -- ^ whether to check balance assertions    -- mutable   ,bsBalances     :: H.HashTable s AccountName MixedAmount      -- ^ running account balances, initially empty@@ -422,8 +422,8 @@     -- display precisions used in balanced checking     styles = Just $ journalCommodityStyles j     bopts = bopts'{commodity_styles_=styles}-    -- balance assignments will not be allowed on these-    txnmodifieraccts = S.fromList . map (paccount . tmprPosting) . concatMap tmpostingrules $ jtxnmodifiers j+    -- balance assignments are not allowed on accounts affected by auto postings+    autopostingaccts = S.fromList . map (paccount . tmprPosting) . concatMap tmpostingrules $ jtxnmodifiers j   in     runST $ do       -- We'll update a mutable array of transactions as we balance them,@@ -448,7 +448,7 @@         -- 2. Sort these items by date, preserving the order of same-day items,         -- and step through them while keeping running account balances,         runningbals <- lift $ H.newSized (length $ journalAccountNamesUsed j)-        flip runReaderT (BalancingState styles txnmodifieraccts (not $ ignore_assertions_ bopts) runningbals balancedtxns) $ do+        flip runReaderT (BalancingState styles autopostingaccts (not $ ignore_assertions_ bopts) runningbals balancedtxns) $ do           -- performing balance assignments in, and balancing, the remaining transactions,           -- and checking balance assertions as each posting is processed.           void $ mapM' balanceTransactionAndCheckAssertionsB $ sortOn (either postingDate tdate) psandts@@ -618,30 +618,29 @@ checkBalanceAssignmentPostingDateB :: Posting -> Balancing s () checkBalanceAssignmentPostingDateB p =   when (hasBalanceAssignment p && isJust (pdate p)) $-    throwError . T.unpack $ T.unlines-      ["postings which are balance assignments may not have a custom date."-      ,"Please write the posting amount explicitly, or remove the posting date:"+    throwError $ chomp $ unlines [+       "can't use balance assignment with custom posting date"       ,""-      ,maybe (T.unlines $ showPostingLines p) showTransaction $ ptransaction p+      ,chomp1 $ T.unpack $ maybe (T.unlines $ showPostingLines p) showTransaction $ ptransaction p+      ,"Balance assignments may not be used on postings with a custom posting date"+      ,"(it makes balancing the journal impossible)."+      ,"Please write the posting amount explicitly (or remove the posting date)."       ]  -- | Throw an error if this posting is trying to do a balance assignment and -- the account does not allow balance assignments (eg because it is referenced--- by a transaction modifier, which might generate additional postings to it).+-- by an auto posting rule, which might generate additional postings to it). checkBalanceAssignmentUnassignableAccountB :: Posting -> Balancing s () checkBalanceAssignmentUnassignableAccountB p = do   unassignable <- R.asks bsUnassignable   when (hasBalanceAssignment p && paccount p `S.member` unassignable) $-    throwError . T.unpack $ T.unlines-      ["balance assignments cannot be used with accounts which are"-      ,"posted to by transaction modifier rules (auto postings)."-      ,"Please write the posting amount explicitly, or remove the rule."-      ,""-      ,"account: " <> paccount p-      ,""-      ,"transaction:"+    throwError $ chomp $ unlines [+       "can't use balance assignment with auto postings"       ,""-      ,maybe (T.unlines $ showPostingLines p) showTransaction $ ptransaction p+      ,chomp1 $ T.unpack $ maybe (T.unlines $ showPostingLines p) (showTransaction) $ ptransaction p+      ,"Balance assignments may not be used on accounts affected by auto posting rules"+      ,"(it makes balancing the journal impossible)."+      ,"Please write the posting amount explicitly (or remove the auto posting rule(s))."       ]  -- lenses
Hledger/Data/Dates.hs view
@@ -12,7 +12,8 @@ For date and time values, we use the standard Day and UTCTime types.  A 'SmartDate' is a date which may be partially-specified or relative.-Eg 2008\/12\/31, but also 2008\/12, 12\/31, tomorrow, last week, next year.+Eg 2008\/12\/31, but also 2008\/12, 12\/31, tomorrow, last week, next year,+in 5 days, in -3 quarters. We represent these as a triple of strings like (\"2008\",\"12\",\"\"), (\"\",\"\",\"tomorrow\"), (\"\",\"last\",\"week\"). @@ -66,6 +67,7 @@   latestSpanContaining,   smartdate,   splitSpan,+  spansFromBoundaries,   groupByDateSpan,   fixSmartDate,   fixSmartDateStr,@@ -73,7 +75,6 @@   fixSmartDateStrEither',   yearp,   daysInSpan,-  maybePeriod,    tests_Dates )@@ -104,7 +105,7 @@ import Safe (headMay, lastMay, maximumMay, minimumMay) import Text.Megaparsec import Text.Megaparsec.Char (char, char', digitChar, string, string')-import Text.Megaparsec.Char.Lexer (decimal)+import Text.Megaparsec.Char.Lexer (decimal, signed) import Text.Megaparsec.Custom (customErrorBundlePretty) import Text.Printf (printf) @@ -207,52 +208,48 @@ -- splitSpan :: Interval -> DateSpan -> [DateSpan] splitSpan _ (DateSpan Nothing Nothing) = [DateSpan Nothing Nothing]-splitSpan _ s | isEmptySpan s = []-splitSpan NoInterval     s = [s]-splitSpan (Days n)       s = splitspan startofday     (applyN n nextday)     s-splitSpan (Weeks n)      s = splitspan startofweek    (applyN n nextweek)    s-splitSpan (Months n)     s = splitspan startofmonth   (applyN n nextmonth)   s-splitSpan (Quarters n)   s = splitspan startofquarter (applyN n nextquarter) s-splitSpan (Years n)      s = splitspan startofyear    (applyN n nextyear)    s-splitSpan (DayOfMonth n) s = splitspan (nthdayofmonthcontaining n) (nthdayofmonth n . nextmonth) s-splitSpan (WeekdayOfMonth n wd) s = splitspan (nthweekdayofmonthcontaining n wd) (advancetonthweekday n wd . nextmonth) s-splitSpan (DaysOfWeek []) s = [s]  -- shouldn't happen in parser but for completeness-splitSpan (DaysOfWeek days@(n:_)) ds-  | DateSpan Nothing  (Just e)  <- ds = split (DateSpan (Just $ start e) (Just $ nextday $ start e))-  | DateSpan (Just s) Nothing  <- ds = split (DateSpan (Just $ start s) (Just $ nextday $ start s))-  | DateSpan (Just s) (Just e) <- ds =-      if s == e then [ds] else split (DateSpan (Just $ start s) (Just e))+splitSpan _ ds | isEmptySpan ds = []+splitSpan _ ds@(DateSpan (Just s) (Just e)) | s == e = [ds]+splitSpan NoInterval      ds = [ds]+splitSpan (Days n)        ds = splitspan startofday     addDays n                    ds+splitSpan (Weeks n)       ds = splitspan startofweek    addDays (7*n)                ds+splitSpan (Months n)      ds = splitspan startofmonth   addGregorianMonthsClip n     ds+splitSpan (Quarters n)    ds = splitspan startofquarter addGregorianMonthsClip (3*n) ds+splitSpan (Years n)       ds = splitspan startofyear    addGregorianYearsClip n      ds+splitSpan (DayOfMonth n)  ds = splitspan (nthdayofmonthcontaining n)  addGregorianMonthsClip 1 ds+splitSpan (DayOfYear m n) ds = splitspan (nthdayofyearcontaining m n) addGregorianYearsClip 1 ds+splitSpan (WeekdayOfMonth n wd) ds = splitspan (nthweekdayofmonthcontaining n wd) advancemonths 1 ds   where-    start = nthdayofweekcontaining n--    wheel = (\x -> zipWith (-) (tail x) x) . concat . zipWith fmap (fmap (+) [0,7..]) . repeat $ days--    split = splitspan' (repeat startofday) (fmap (`applyN` nextday) wheel)--splitSpan (DayOfYear m n) s = splitspan (nthdayofyearcontaining m n) (applyN (n-1) nextday . applyN (m-1) nextmonth . nextyear) s--- splitSpan (WeekOfYear n)    s = splitspan startofweek    (applyN n nextweek)    s--- splitSpan (MonthOfYear n)   s = splitspan startofmonth   (applyN n nextmonth)   s--- splitSpan (QuarterOfYear n) s = splitspan startofquarter (applyN n nextquarter) s+    advancemonths 0 = id+    advancemonths w = advancetonthweekday n wd . startofmonth . addGregorianMonthsClip w+splitSpan (DaysOfWeek [])         ds = [ds]+splitSpan (DaysOfWeek days@(n:_)) ds = spansFromBoundaries e bdrys+  where+    (s, e) = dateSpanSplitLimits (nthdayofweekcontaining n) nextday ds+    bdrys = concatMap (flip map starts . addDays) [0,7..]+    -- The first representative of each weekday+    starts = map (\d -> addDays (toInteger $ d - n) $ nthdayofweekcontaining n s) days  -- Split the given span using the provided helper functions: -- start is applied to the span's start date to get the first sub-span's start date--- next is applied to a sub-span's start date to get the next sub-span's start date-splitspan :: (Day -> Day) -> (Day -> Day) -> DateSpan -> [DateSpan]-splitspan _ _ (DateSpan Nothing Nothing) = []-splitspan start next (DateSpan Nothing (Just e)) = splitspan start next (DateSpan (Just $ start e) (Just $ next $ start e))-splitspan start next (DateSpan (Just s) Nothing) = splitspan start next (DateSpan (Just $ start s) (Just $ next $ start s))-splitspan start next span@(DateSpan (Just s) (Just e))-    | s == e = [span]-    | otherwise = splitspan' (repeat start) (repeat next) span+-- addInterval is applied to an integer n (multiplying it by mult) and the span's start date to get the nth sub-span's start date+splitspan :: (Day -> Day) -> (Integer -> Day -> Day) -> Int -> DateSpan -> [DateSpan]+splitspan start addInterval mult ds = spansFromBoundaries e bdrys+  where+    (s, e) = dateSpanSplitLimits start (addInterval $ toInteger mult) ds+    bdrys = mapM (addInterval . toInteger) [0,mult..] $ start s -splitspan' :: [Day -> Day] -> [Day -> Day] -> DateSpan -> [DateSpan]-splitspan' (start:ss) (next:ns) (DateSpan (Just s) (Just e))-    | s >= e = []-    | otherwise = DateSpan (Just subs) (Just sube) : splitspan' ss ns (DateSpan (Just sube) (Just e))-    where subs = start s-          sube = next subs-splitspan' _ _ _ = error' "won't happen, avoids warnings"  -- PARTIAL:+-- | Fill in missing endpoints for calculating 'splitSpan'.+dateSpanSplitLimits :: (Day -> Day) -> (Day -> Day) -> DateSpan -> (Day, Day)+dateSpanSplitLimits start _    (DateSpan (Just s) (Just e)) = (start s, e)+dateSpanSplitLimits start next (DateSpan (Just s) Nothing)  = (start s, next $ start s)+dateSpanSplitLimits start next (DateSpan Nothing  (Just e)) = (start e, next $ start e)+dateSpanSplitLimits _     _    (DateSpan Nothing   Nothing) = error "dateSpanSplitLimits: Should not be nulldatespan"  -- PARTIAL: This case should have been handled in splitSpan +-- | Construct a list of 'DateSpan's from a list of boundaries, which fit within a given range.+spansFromBoundaries :: Day -> [Day] -> [DateSpan]+spansFromBoundaries e bdrys = zipWith (DateSpan `on` Just) (takeWhile (< e) bdrys) $ drop 1 bdrys+ -- | Count the days in a DateSpan, or if it is open-ended return Nothing. daysInSpan :: DateSpan -> Maybe Integer daysInSpan (DateSpan (Just d1) (Just d2)) = Just $ diffDays d2 d1@@ -373,9 +370,6 @@   either (error' . ("failed to parse:" ++) . customErrorBundlePretty) id $  -- PARTIAL:   parsePeriodExpr refdate s -maybePeriod :: Day -> Text -> Maybe (Interval,DateSpan)-maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate- -- | Show a DateSpan as a human-readable pseudo-period-expression string. -- dateSpanAsText :: DateSpan -> String -- dateSpanAsText (DateSpan Nothing Nothing)   = "all"@@ -396,21 +390,11 @@       (ry,rm,_) = toGregorian refdate       (b,e) = span sdate       span :: SmartDate -> (Day,Day)-      span (SmartRelative This Day)                 = (refdate, nextday refdate)-      span (SmartRelative Last Day)                 = (prevday refdate, refdate)-      span (SmartRelative Next Day)                 = (nextday refdate, addDays 2 refdate)-      span (SmartRelative This Week)                = (thisweek refdate, nextweek refdate)-      span (SmartRelative Last Week)                = (prevweek refdate, thisweek refdate)-      span (SmartRelative Next Week)                = (nextweek refdate, startofweek $ addDays 14 refdate)-      span (SmartRelative This Month)               = (thismonth refdate, nextmonth refdate)-      span (SmartRelative Last Month)               = (prevmonth refdate, thismonth refdate)-      span (SmartRelative Next Month)               = (nextmonth refdate, startofmonth $ addGregorianMonthsClip 2 refdate)-      span (SmartRelative This Quarter)             = (thisquarter refdate, nextquarter refdate)-      span (SmartRelative Last Quarter)             = (prevquarter refdate, thisquarter refdate)-      span (SmartRelative Next Quarter)             = (nextquarter refdate, startofquarter $ addGregorianMonthsClip 6 refdate)-      span (SmartRelative This Year)                = (thisyear refdate, nextyear refdate)-      span (SmartRelative Last Year)                = (prevyear refdate, thisyear refdate)-      span (SmartRelative Next Year)                = (nextyear refdate, startofyear $ addGregorianYearsClip 2 refdate)+      span (SmartRelative n Day)                    = (addDays n refdate, addDays (n+1) refdate)+      span (SmartRelative n Week)                   = let d = thisweek refdate in (addDays (7*n) d, addDays (7*n+7) d)+      span (SmartRelative n Month)                  = let d = thismonth refdate in (addGregorianMonthsClip n d, addGregorianMonthsClip (n+1) d)+      span (SmartRelative n Quarter)                = let d = thisquarter refdate in (addGregorianMonthsClip (3*n) d, addGregorianMonthsClip (3*n+3) d)+      span (SmartRelative n Year)                   = let d = thisyear refdate in (addGregorianYearsClip n d, addGregorianYearsClip (n+1) d)       span (SmartAssumeStart y Nothing)             = (startofyear day, nextyear day) where day = fromGregorian y 1 1       span (SmartAssumeStart y (Just (m, Nothing))) = (startofmonth day, nextmonth day) where day = fromGregorian y m 1       span (SmartAssumeStart y (Just (m, Just d)))  = (day, nextday day) where day = fromGregorian y m d@@ -510,28 +494,28 @@ -- t "next january" -- "2009-01-01" --+-- >>> t "in 5 days"+-- "2008-12-01"+-- >>> t "in 7 months"+-- "2009-06-01"+-- >>> t "in -2 weeks"+-- "2008-11-10"+-- >>> t "1 quarter ago"+-- "2008-07-01"+-- >>> t "1 week ahead"+-- "2008-12-01" fixSmartDate :: Day -> SmartDate -> Day fixSmartDate refdate = fix   where     fix :: SmartDate -> Day-    fix (SmartRelative This Day)     = refdate-    fix (SmartRelative Last Day)     = prevday refdate-    fix (SmartRelative Next Day)     = nextday refdate-    fix (SmartRelative This Week)    = thisweek refdate-    fix (SmartRelative Last Week)    = prevweek refdate-    fix (SmartRelative Next Week)    = nextweek refdate-    fix (SmartRelative This Month)   = thismonth refdate-    fix (SmartRelative Last Month)   = prevmonth refdate-    fix (SmartRelative Next Month)   = nextmonth refdate-    fix (SmartRelative This Quarter) = thisquarter refdate-    fix (SmartRelative Last Quarter) = prevquarter refdate-    fix (SmartRelative Next Quarter) = nextquarter refdate-    fix (SmartRelative This Year)    = thisyear refdate-    fix (SmartRelative Last Year)    = prevyear refdate-    fix (SmartRelative Next Year)    = nextyear refdate-    fix (SmartAssumeStart y md)      = fromGregorian y (maybe 1 fst md) (fromMaybe 1 $ snd =<< md)-    fix (SmartFromReference m d)     = fromGregorian ry (fromMaybe rm m) d-    fix (SmartMonth m)               = fromGregorian ry m 1+    fix (SmartRelative n Day)     = addDays n refdate+    fix (SmartRelative n Week)    = addDays (7*n) $ thisweek refdate+    fix (SmartRelative n Month)   = addGregorianMonthsClip n $ thismonth refdate+    fix (SmartRelative n Quarter) = addGregorianMonthsClip (3*n) $ thisquarter refdate+    fix (SmartRelative n Year)    = addGregorianYearsClip n $ thisyear refdate+    fix (SmartAssumeStart y md)   = fromGregorian y (maybe 1 fst md) (fromMaybe 1 $ snd =<< md)+    fix (SmartFromReference m d)  = fromGregorian ry (fromMaybe rm m) d+    fix (SmartMonth m)            = fromGregorian ry m 1     (ry, rm, _) = toGregorian refdate  prevday :: Day -> Day@@ -554,8 +538,6 @@ nthdayofmonth d day = fromGregorian y m d where (y,m,_) = toGregorian day  thisquarter = startofquarter-prevquarter = startofquarter . addGregorianMonthsClip (-3)-nextquarter = startofquarter . addGregorianMonthsClip 3 startofquarter day = fromGregorian y (firstmonthofquarter m) 1     where       (y,m,_) = toGregorian day@@ -726,6 +708,8 @@ > october, oct                                (start of month in current year) > yesterday, today, tomorrow                  (-1, 0, 1 days from today) > last/this/next day/week/month/quarter/year  (-1, 0, 1 periods from the current period)+> in n days/weeks/months/quarters/years       (n periods from the current period)+> n days/weeks/months/quarters/years ago      (-n periods from the current period) > 20181201                                    (8 digit YYYYMMDD with valid year month and day) > 201812                                      (6 digit YYYYMM with valid year and month) @@ -763,20 +747,27 @@ smartdate :: TextParser m SmartDate smartdate = choice'   -- XXX maybe obscures date errors ? see ledgerdate-    [ yyyymmdd, ymd+    [ relativeP+    , yyyymmdd, ymd     , (\(m,d) -> SmartFromReference (Just m) d) <$> md     , failIfInvalidDate . SmartFromReference Nothing =<< decimal     , SmartMonth <$> (month <|> mon)-    , SmartRelative This Day <$ string' "today"-    , SmartRelative Last Day <$ string' "yesterday"-    , SmartRelative Next Day <$ string' "tomorrow"-    , liftA2 SmartRelative (seqP <* skipNonNewlineSpaces) intervalP+    , SmartRelative 0    Day <$ string' "today"+    , SmartRelative (-1) Day <$ string' "yesterday"+    , SmartRelative 1    Day <$ string' "tomorrow"     ]   where-    seqP = choice [This <$ string' "this", Last <$ string' "last", Next <$ string' "next"]-    intervalP = choice [Day <$ string' "day", Week <$ string' "week", Month <$ string' "month",-                        Quarter <$ string' "quarter", Year <$ string' "year"]+    relativeP = do+        optional $ string' "in" <* skipNonNewlineSpaces+        num      <- seqP <* skipNonNewlineSpaces+        interval <- intervalP <* skipNonNewlineSpaces+        sign     <- choice [negate <$ string' "ago", id <$ string' "ahead", pure id]+        return $ SmartRelative (sign num) interval +    seqP = choice [ 0 <$ string' "this", -1 <$ string' "last", 1 <$ string' "next", signed skipNonNewlineSpaces decimal ]+    intervalP = choice [ Day <$ string' "day", Week <$ string' "week", Month <$ string' "month"+                       , Quarter <$ string' "quarter", Year <$ string' "year" ] <* optional (char' 's')+ -- | Like smartdate, but there must be nothing other than whitespace after the date. smartdateonly :: TextParser m SmartDate smartdateonly = smartdate <* skipNonNewlineSpaces <* eof@@ -968,18 +959,14 @@                                               (toPermutation $ nth <* skipNonNewlineSpaces)      -- Parse any of several variants of a basic interval, eg "daily", "every day", "every N days".-    tryinterval :: String -> String -> (Int -> Interval) -> TextParser m Interval+    tryinterval :: Text -> Text -> (Int -> Interval) -> TextParser m Interval     tryinterval singular compact intcons = intcons <$> choice'-        [ 1 <$ string' compact'+        [ 1 <$ string' compact         , string' "every" *> skipNonNewlineSpaces *> choice-            [ 1 <$ string' singular'-            , decimal <* skipNonNewlineSpaces <* string' plural'+            [ 1 <$ string' singular+            , decimal <* skipNonNewlineSpaces <* string' (singular <> "s")             ]         ]-      where-        compact'  = T.pack compact-        singular' = T.pack singular-        plural'   = T.pack $ singular ++ "s"  periodexprdatespanp :: Day -> TextParser m DateSpan periodexprdatespanp rdate = choice $ map try [@@ -1078,8 +1065,8 @@             ]    , testCase "match dayOfWeek" $ do-      let dayofweek n s = splitspan (nthdayofweekcontaining n) (applyN (n-1) nextday . nextweek) s-          match ds day = dayofweek day ds == splitSpan (DaysOfWeek [day]) ds @?= True+      let dayofweek n s = splitspan (nthdayofweekcontaining n) (\w -> (if w == 0 then id else applyN (n-1) nextday . applyN (fromInteger w) nextweek)) 1 s+          match ds day = splitSpan (DaysOfWeek [day]) ds @?= dayofweek day ds           ys2021 = fromGregorian 2021 01 01           ye2021 = fromGregorian 2021 12 31           ys2022 = fromGregorian 2022 01 01
Hledger/Data/Journal.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE Rank2Types          #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections       #-}  {-| @@ -24,6 +25,7 @@   commodityStylesFromAmounts,   journalCommodityStyles,   journalToCost,+  journalAddInferredEquityPostings,   journalReverse,   journalSetLastReadTime,   journalPivot,@@ -46,7 +48,12 @@   journalAccountNamesDeclared,   journalAccountNamesDeclaredOrUsed,   journalAccountNamesDeclaredOrImplied,+  journalLeafAccountNamesDeclared,   journalAccountNames,+  journalLeafAccountNames,+  journalAccountNameTree,+  journalAccountTags,+  journalInheritedAccountTags,   -- journalAmountAndPriceCommodities,   -- journalAmountStyles,   -- overJournalAmounts,@@ -70,6 +77,11 @@   journalPrevTransaction,   journalPostings,   journalTransactionsSimilarTo,+  -- * Account types+  journalAccountType,+  journalAccountTypes,+  journalAddAccountTypes,+  journalPostingsAddAccountTags,   -- journalPrices,   -- * Standard account types   journalBalanceSheetAccountQuery,@@ -80,6 +92,7 @@   journalLiabilityAccountQuery,   journalEquityAccountQuery,   journalCashAccountQuery,+  journalConversionAccount,   -- * Misc   canonicalStyleFrom,   nulljournal,@@ -92,7 +105,8 @@   samplejournal,   samplejournalMaybeExplicit,   tests_Journal-,journalLeafAccountNamesDeclared)+  --+) where  import Control.Applicative ((<|>))@@ -101,7 +115,7 @@ import Data.Char (toUpper, isDigit) import Data.Default (Default(..)) import Data.Foldable (toList)-import Data.List ((\\), find, foldl', sortBy)+import Data.List ((\\), find, foldl', sortBy, union) import Data.List.Extra (nubSort) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, mapMaybe, maybeToList)@@ -111,7 +125,7 @@ import Safe (headMay, headDef, maximumMay, minimumMay) import Data.Time.Calendar (Day, addDays, fromGregorian) import Data.Time.Clock.POSIX (POSIXTime)-import Data.Tree (Tree, flatten)+import Data.Tree (Tree(..), flatten) import Text.Printf (printf) import Text.Megaparsec (ParsecT) import Text.Megaparsec.Custom (FinalParseError)@@ -120,9 +134,10 @@ import Hledger.Data.Types import Hledger.Data.AccountName import Hledger.Data.Amount+import Hledger.Data.Posting import Hledger.Data.Transaction import Hledger.Data.TransactionModifier-import Hledger.Data.Posting+import Hledger.Data.Valuation import Hledger.Query  @@ -193,7 +208,9 @@     ,jincludefilestack          = jincludefilestack j2     ,jdeclaredpayees            = jdeclaredpayees            j1 <> jdeclaredpayees            j2     ,jdeclaredaccounts          = jdeclaredaccounts          j1 <> jdeclaredaccounts          j2+    ,jdeclaredaccounttags       = jdeclaredaccounttags       j1 <> jdeclaredaccounttags       j2     ,jdeclaredaccounttypes      = jdeclaredaccounttypes      j1 <> jdeclaredaccounttypes      j2+    ,jaccounttypes              = jaccounttypes              j1 <> jaccounttypes              j2     ,jglobalcommoditystyles     = jglobalcommoditystyles     j1 <> jglobalcommoditystyles     j2     ,jcommodities               = jcommodities               j1 <> jcommodities               j2     ,jinferredcommodities       = jinferredcommodities       j1 <> jinferredcommodities       j2@@ -222,7 +239,9 @@   ,jincludefilestack          = []   ,jdeclaredpayees            = []   ,jdeclaredaccounts          = []+  ,jdeclaredaccounttags       = M.empty   ,jdeclaredaccounttypes      = M.empty+  ,jaccounttypes              = M.empty   ,jglobalcommoditystyles     = M.empty   ,jcommodities               = M.empty   ,jinferredcommodities       = M.empty@@ -334,9 +353,26 @@ journalAccountNames :: Journal -> [AccountName] journalAccountNames = journalAccountNamesDeclaredOrImplied +-- | Sorted unique account names declared or implied in this journal+-- which have no children.+journalLeafAccountNames :: Journal -> [AccountName]+journalLeafAccountNames = treeLeaves . journalAccountNameTree+ journalAccountNameTree :: Journal -> Tree AccountName journalAccountNameTree = accountNameTreeFrom . journalAccountNamesDeclaredOrImplied +-- | Which tags have been declared explicitly for this account, if any ?+journalAccountTags :: Journal -> AccountName -> [Tag]+journalAccountTags Journal{jdeclaredaccounttags} a = M.findWithDefault [] a jdeclaredaccounttags++-- | Which tags are in effect for this account, including tags inherited from parent accounts ?+journalInheritedAccountTags :: Journal -> AccountName -> [Tag]+journalInheritedAccountTags j a =+  foldl' (\ts a -> ts `union` journalAccountTags j a) [] as+  where+    as = a : parentAccountNames a+-- PERF: cache in journal ?+ -- | Find up to N most similar and most recent transactions matching -- the given transaction description and query. Transactions are -- listed with their description's similarity score (see@@ -395,13 +431,16 @@ -- queries for standard account types  -- | Get a query for accounts of the specified types in this journal. --- Account types include Asset, Liability, Equity, Revenue, Expense, Cash.+-- Account types include:+-- Asset, Liability, Equity, Revenue, Expense, Cash, Conversion. -- For each type, if no accounts were declared with this type, the query  -- will instead match accounts with names matched by the case-insensitive  -- regular expression provided as a fallback. -- The query will match all accounts which were declared as one of -- these types (by account directives with the type: tag), plus all their  -- subaccounts which have not been declared as some other type.+--+-- This is older code pre-dating 2022's expansion of account types. journalAccountTypeQuery :: [AccountType] -> Regexp -> Journal -> Query journalAccountTypeQuery atypes fallbackregex Journal{jdeclaredaccounttypes} =   let@@ -427,69 +466,55 @@ -- or otherwise for accounts with names matched by the case-insensitive  -- regular expression @^assets?(:|$)@. journalAssetAccountQuery :: Journal -> Query-journalAssetAccountQuery j = +journalAssetAccountQuery j =   Or [-     journalAccountTypeQuery [Asset] (toRegexCI' "^assets?(:|$)") j+     journalAccountTypeQuery [Asset] assetAccountRegex j     ,journalCashAccountOnlyQuery j   ] --- | A query for accounts in this journal which have been--- declared as Asset (and not Cash) by account directives, --- or otherwise for accounts with names matched by the case-insensitive --- regular expression @^assets?(:|$)@.-journalAssetNonCashAccountQuery :: Journal -> Query-journalAssetNonCashAccountQuery = journalAccountTypeQuery [Asset] (toRegexCI' "^assets?(:|$)")- -- | A query for Cash (liquid asset) accounts in this journal, ie accounts--- declared as Cash by account directives, or otherwise Asset accounts whose --- names do not include the case-insensitive regular expression --- @(investment|receivable|:A/R|:fixed)@.-journalCashAccountQuery  :: Journal -> Query-journalCashAccountQuery j =-  case M.lookup Cash (jdeclaredaccounttypes j) of-    Just _  -> journalCashAccountOnlyQuery j-    Nothing ->-      -- no Cash accounts are declared; query for Asset accounts and exclude some of them-      And [ journalAssetNonCashAccountQuery j, Not . Acct $ toRegexCI' "(investment|receivable|:A/R|:fixed)" ]+-- declared as Cash by account directives, or otherwise accounts whose+-- names match the case-insensitive regular expression+-- @(^assets:(.+:)?(cash|bank)(:|$)@.+journalCashAccountQuery :: Journal -> Query+journalCashAccountQuery = journalAccountTypeQuery [Cash] cashAccountRegex  -- | A query for accounts in this journal specifically declared as Cash by  -- account directives, or otherwise the None query.-journalCashAccountOnlyQuery  :: Journal -> Query-journalCashAccountOnlyQuery j = -  case M.lookup Cash (jdeclaredaccounttypes j) of-    Just _  -> -      -- Cash accounts are declared; get a query for them (the fallback regex won't be used)-      journalAccountTypeQuery [Cash] notused j-        where notused = error' "journalCashAccountOnlyQuery: this should not have happened!"  -- PARTIAL:-    Nothing -> None+journalCashAccountOnlyQuery :: Journal -> Query+journalCashAccountOnlyQuery j+  -- Cash accounts are declared; get a query for them (the fallback regex won't be used)+  | Cash `M.member` jdeclaredaccounttypes j = journalAccountTypeQuery [Cash] notused j+  | otherwise = None+  where notused = error' "journalCashAccountOnlyQuery: this should not have happened!"  -- PARTIAL:  -- | A query for accounts in this journal which have been -- declared as Liability by account directives, or otherwise for -- accounts with names matched by the case-insensitive regular expression -- @^(debts?|liabilit(y|ies))(:|$)@. journalLiabilityAccountQuery :: Journal -> Query-journalLiabilityAccountQuery = journalAccountTypeQuery [Liability] (toRegexCI' "^(debts?|liabilit(y|ies))(:|$)")+journalLiabilityAccountQuery = journalAccountTypeQuery [Liability] liabilityAccountRegex  -- | A query for accounts in this journal which have been -- declared as Equity by account directives, or otherwise for -- accounts with names matched by the case-insensitive regular expression -- @^equity(:|$)@. journalEquityAccountQuery :: Journal -> Query-journalEquityAccountQuery = journalAccountTypeQuery [Equity] (toRegexCI' "^equity(:|$)")+journalEquityAccountQuery = journalAccountTypeQuery [Equity] equityAccountRegex  -- | A query for accounts in this journal which have been -- declared as Revenue by account directives, or otherwise for -- accounts with names matched by the case-insensitive regular expression -- @^(income|revenue)s?(:|$)@. journalRevenueAccountQuery :: Journal -> Query-journalRevenueAccountQuery = journalAccountTypeQuery [Revenue] (toRegexCI' "^(income|revenue)s?(:|$)")+journalRevenueAccountQuery = journalAccountTypeQuery [Revenue] revenueAccountRegex  -- | A query for accounts in this journal which have been -- declared as Expense by account directives, or otherwise for -- accounts with names matched by the case-insensitive regular expression -- @^expenses?(:|$)@. journalExpenseAccountQuery  :: Journal -> Query-journalExpenseAccountQuery = journalAccountTypeQuery [Expense] (toRegexCI' "^expenses?(:|$)")+journalExpenseAccountQuery = journalAccountTypeQuery [Expense] expenseAccountRegex  -- | A query for Asset, Liability & Equity accounts in this journal. -- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Balance_Sheet_Accounts>.@@ -506,6 +531,54 @@                                         ,journalExpenseAccountQuery j                                         ] +-- | The 'AccountName' to use for automatically generated conversion postings.+journalConversionAccount :: Journal -> AccountName+journalConversionAccount =+    headDef (T.pack "equity:conversion")+    . M.findWithDefault [] Conversion+    . jdeclaredaccounttypes++-- Newer account type functionality.++journalAccountType :: Journal -> AccountName -> Maybe AccountType+journalAccountType Journal{jaccounttypes} = accountNameType jaccounttypes++-- | Add a map of all known account types to the journal.+journalAddAccountTypes :: Journal -> Journal+journalAddAccountTypes j = j{jaccounttypes = journalAccountTypes j}++-- | Build a map of all known account types, explicitly declared+-- or inferred from the account's parent or name.+journalAccountTypes :: Journal -> M.Map AccountName AccountType+journalAccountTypes j = M.fromList [(a,acctType) | (a, Just (acctType,_)) <- flatten t']+  where+    t = accountNameTreeFrom $ journalAccountNames j :: Tree AccountName+    t' = settypes Nothing t :: Tree (AccountName, Maybe (AccountType, Bool))+    -- Map from the top of the account tree down to the leaves, propagating+    -- account types downward. Keep track of whether the account is declared+    -- (True), in which case the parent account should be preferred, or merely+    -- inferred (False), in which case the inferred type should be preferred.+    settypes :: Maybe (AccountType, Bool) -> Tree AccountName -> Tree (AccountName, Maybe (AccountType, Bool))+    settypes mparenttype (Node a subs) = Node (a, mtype) (map (settypes mtype) subs)+      where+        mtype = M.lookup a declaredtypes <|> minferred+        minferred = if maybe False snd mparenttype+                       then mparenttype+                       else (,False) <$> accountNameInferType a <|> mparenttype+    declaredtypes = (,True) <$> journalDeclaredAccountTypes j++-- | Build a map of the account types explicitly declared.+journalDeclaredAccountTypes :: Journal -> M.Map AccountName AccountType+journalDeclaredAccountTypes Journal{jdeclaredaccounttypes} =+  M.fromList $ concat [map (,t) as | (t,as) <- M.toList jdeclaredaccounttypes]++-- | To all postings in the journal, add any tags from their account+-- (including those inherited from parent accounts).+-- If the same tag exists on posting and account, the latter is ignored.+journalPostingsAddAccountTags :: Journal -> Journal+journalPostingsAddAccountTags j = journalMapPostings addtags j+  where addtags p = p `postingAddTags` (journalInheritedAccountTags j $ paccount p)+ -- Various kinds of filtering on journals. We do it differently depending -- on the command. @@ -514,12 +587,12 @@  -- | Keep only transactions matching the query expression. filterJournalTransactions :: Query -> Journal -> Journal-filterJournalTransactions q j@Journal{jtxns=ts} = j{jtxns=filter (q `matchesTransaction`) ts}+filterJournalTransactions q j@Journal{jtxns} = j{jtxns=filter (matchesTransactionExtra (journalAccountType j) q) jtxns}  -- | Keep only postings matching the query expression. -- This can leave unbalanced transactions. filterJournalPostings :: Query -> Journal -> Journal-filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionPostings q) ts}+filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionPostingsExtra (journalAccountType j) q) ts}  -- | Keep only postings which do not match the query expression, but for which a related posting does. -- This can leave unbalanced transactions.@@ -550,6 +623,11 @@ filterTransactionPostings :: Query -> Transaction -> Transaction filterTransactionPostings q t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps} +-- Like filterTransactionPostings, but is given the map of account types so can also filter by account type.+filterTransactionPostingsExtra :: (AccountName -> Maybe AccountType) -> Query -> Transaction -> Transaction+filterTransactionPostingsExtra atypes q t@Transaction{tpostings=ps} =+  t{tpostings=filter (matchesPostingExtra atypes q) ps}+ filterTransactionRelatedPostings :: Query -> Transaction -> Transaction filterTransactionRelatedPostings q t@Transaction{tpostings=ps} =     t{tpostings=if null matches then [] else ps \\ matches}@@ -732,7 +810,7 @@ -- relative dates in transaction modifier queries. journalModifyTransactions :: Day -> Journal -> Either String Journal journalModifyTransactions d j =-    case modifyTransactions (journalCommodityStyles j) d (jtxnmodifiers j) (jtxns j) of+    case modifyTransactions (journalAccountType j) (journalInheritedAccountTags j) (journalCommodityStyles j) d (jtxnmodifiers j) (jtxns j) of       Right ts -> Right j{jtxns=ts}       Left err -> Left err @@ -870,11 +948,17 @@  -- | Convert all this journal's amounts to cost using the transaction prices, if any. -- The journal's commodity styles are applied to the resulting amounts.-journalToCost :: Journal -> Journal-journalToCost j@Journal{jtxns=ts} = j{jtxns=map (transactionToCost styles) ts}-    where-      styles = journalCommodityStyles j+journalToCost :: ConversionOp -> Journal -> Journal+journalToCost cost j@Journal{jtxns=ts} = j{jtxns=map (transactionToCost styles cost) ts}+  where+    styles = journalCommodityStyles j +-- | Add inferred equity postings to a 'Journal' using transaction prices.+journalAddInferredEquityPostings :: Journal -> Journal+journalAddInferredEquityPostings j = journalMapTransactions (transactionAddInferredEquityPostings equityAcct) j+  where+    equityAcct = journalConversionAccount j+ -- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol. -- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol -- journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountCommodities j@@ -1221,7 +1305,7 @@       namesfrom qfunc = journalAccountNamesMatching (qfunc j) j     in [testCase "assets"      $ assertEqual "" ["assets","assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]          (namesfrom journalAssetAccountQuery)-       ,testCase "cash"        $ assertEqual "" ["assets","assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]+       ,testCase "cash"        $ assertEqual "" ["assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]          (namesfrom journalCashAccountQuery)        ,testCase "liabilities" $ assertEqual "" ["liabilities","liabilities:debts"]          (namesfrom journalLiabilityAccountQuery)
Hledger/Data/PeriodicTransaction.hs view
@@ -263,7 +263,7 @@     _                    -> Nothing     where       checkStart d x =-        let firstDate = fixSmartDate d $ SmartRelative This x+        let firstDate = fixSmartDate d $ SmartRelative 0 x         in          if d == firstDate          then Nothing
Hledger/Data/Posting.hs view
@@ -39,6 +39,7 @@   postingStripPrices,   postingApplyAliases,   postingApplyCommodityStyles,+  postingAddTags,   -- * date operations   postingDate,   postingDate2,@@ -47,13 +48,6 @@   isPostingInDateSpan',   -- * account name operations   accountNamesFromPostings,-  accountNamePostingType,-  accountNameWithoutPostingType,-  accountNameWithPostingType,-  joinAccountNames,-  concatAccountNames,-  accountNameApplyAliases,-  accountNameApplyAliasesMemo,   -- * comment/tag operations   commentJoin,   commentAddTag,@@ -71,24 +65,23 @@   postingTransformAmount,   postingApplyValuation,   postingToCost,+  postingAddInferredEquityPostings,   tests_Posting ) where -import Control.Monad (foldM) import Data.Default (def) import Data.Foldable (asum) import qualified Data.Map as M import Data.Maybe (fromMaybe, isJust)-import Data.MemoUgly (memo)-import Data.List (foldl')+import Data.List (foldl', sort, union) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TB import Data.Time.Calendar (Day)-import Safe (headDef, maximumBound)+import Safe (maximumBound) import Text.DocLayout (realLength)  import Text.Tabular.AsciiWide@@ -393,38 +386,6 @@ isEmptyPosting :: Posting -> Bool isEmptyPosting = mixedAmountLooksZero . pamount --- AccountName stuff that depends on PostingType--accountNamePostingType :: AccountName -> PostingType-accountNamePostingType a-    | T.null a = RegularPosting-    | T.head a == '[' && T.last a == ']' = BalancedVirtualPosting-    | T.head a == '(' && T.last a == ')' = VirtualPosting-    | otherwise = RegularPosting--accountNameWithoutPostingType :: AccountName -> AccountName-accountNameWithoutPostingType a = case accountNamePostingType a of-                                    BalancedVirtualPosting -> T.init $ T.tail a-                                    VirtualPosting -> T.init $ T.tail a-                                    RegularPosting -> a--accountNameWithPostingType :: PostingType -> AccountName -> AccountName-accountNameWithPostingType BalancedVirtualPosting = wrap "[" "]" . accountNameWithoutPostingType-accountNameWithPostingType VirtualPosting         = wrap "(" ")" . accountNameWithoutPostingType-accountNameWithPostingType RegularPosting         = accountNameWithoutPostingType---- | Prefix one account name to another, preserving posting type--- indicators like concatAccountNames.-joinAccountNames :: AccountName -> AccountName -> AccountName-joinAccountNames a b = concatAccountNames $ filter (not . T.null) [a,b]---- | Join account names into one. If any of them has () or [] posting type--- indicators, these (the first type encountered) will also be applied to--- the resulting account name.-concatAccountNames :: [AccountName] -> AccountName-concatAccountNames as = accountNameWithPostingType t $ T.intercalate ":" $ map accountNameWithoutPostingType as-    where t = headDef RegularPosting $ filter (/= RegularPosting) $ map accountNamePostingType as- -- | Apply some account aliases to the posting's account name, as described by accountNameApplyAliases. -- This can fail due to a bad replacement pattern in a regular expression alias. postingApplyAliases :: [AccountAlias] -> Posting -> Either RegexError Posting@@ -444,34 +405,9 @@   where     fixbalanceassertion ba = ba{baamount=styleAmountExceptPrecision styles $ baamount ba} --- | Rewrite an account name using all matching aliases from the given list, in sequence.--- Each alias sees the result of applying the previous aliases.--- Or, return any error arising from a bad regular expression in the aliases.-accountNameApplyAliases :: [AccountAlias] -> AccountName -> Either RegexError AccountName-accountNameApplyAliases aliases a =-  let (aname,atype) = (accountNameWithoutPostingType a, accountNamePostingType a)-  in foldM-     (\acct alias -> dbg6 "result" $ aliasReplace (dbg6 "alias" alias) (dbg6 "account" acct))-     aname-     aliases-     >>= Right . accountNameWithPostingType atype---- | Memoising version of accountNameApplyAliases, maybe overkill.-accountNameApplyAliasesMemo :: [AccountAlias] -> AccountName -> Either RegexError AccountName-accountNameApplyAliasesMemo aliases = memo (accountNameApplyAliases aliases)-  -- XXX re-test this memoisation---- aliasMatches :: AccountAlias -> AccountName -> Bool--- aliasMatches (BasicAlias old _) a = old `isAccountNamePrefixOf` a--- aliasMatches (RegexAlias re  _) a = regexMatchesCI re a--aliasReplace :: AccountAlias -> AccountName -> Either RegexError AccountName-aliasReplace (BasicAlias old new) a-  | old `isAccountNamePrefixOf` a || old == a =-      Right $ new <> T.drop (T.length old) a-  | otherwise = Right a-aliasReplace (RegexAlias re repl) a =-  fmap T.pack . regexReplace re repl $ T.unpack a -- XXX+-- | Add tags to a posting, discarding any for which the posting already has a value.+postingAddTags :: Posting -> [Tag] -> Posting+postingAddTags p@Posting{ptags} tags = p{ptags=ptags `union` tags}  -- | Apply a specified valuation to this posting's amount, using the -- provided price oracle, commodity styles, and reference dates.@@ -480,9 +416,45 @@ postingApplyValuation priceoracle styles periodlast today v p =     postingTransformAmount (mixedAmountApplyValuation priceoracle styles periodlast today (postingDate p) v) p --- | Convert this posting's amount to cost, and apply the appropriate amount styles.-postingToCost :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting-postingToCost styles = postingTransformAmount (styleMixedAmount styles . mixedAmountCost)+-- | Maybe convert this 'Posting's amount to cost, and apply apply appropriate+-- amount styles.+postingToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Posting+postingToCost _      NoConversionOp p = p+postingToCost styles ToCost         p = postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p++-- | Generate inferred equity postings from a 'Posting' using transaction prices.+postingAddInferredEquityPostings :: Text -> Posting -> [Posting]+postingAddInferredEquityPostings equityAcct p = taggedPosting : concatMap conversionPostings priceAmounts+  where+    taggedPosting+      | null priceAmounts = p+      | otherwise         = p{ pcomment = pcomment p `commentAddTag` priceTag+                             , ptags = priceTag : ptags p+                             }+    conversionPostings amt = case aprice amt of+        Nothing -> []+        Just _  -> [ cp{ paccount = accountPrefix <> amtCommodity+                       , pamount = mixedAmount . negate $ amountStripPrices amt+                       }+                   , cp{ paccount = accountPrefix <> costCommodity+                       , pamount = mixedAmount cost+                       }+                   ]+      where+        cost = amountCost amt+        amtCommodity  = commodity amt+        costCommodity = commodity cost+        cp = p{ pcomment = pcomment p `commentAddTag` ("generated-posting","")+              , ptags = [("generated-posting", ""), ("_generated-posting", "")]+              , pbalanceassertion = Nothing+              , poriginal = Nothing+              }+        accountPrefix = mconcat [ equityAcct, ":", T.intercalate "-" $ sort [amtCommodity, costCommodity], ":"]+        -- Take the commodity of an amount and collapse consecutive spaces to a single space+        commodity = T.unwords . filter (not . T.null) . T.words . acommodity++    priceTag = ("cost", T.strip . wbToText $ foldMap showAmountPrice priceAmounts)+    priceAmounts = filter (isJust . aprice) . amountsRaw $ pamount p  -- | Apply a transform function to this posting's amount. postingTransformAmount :: (MixedAmount -> MixedAmount) -> Posting -> Posting
Hledger/Data/Transaction.hs view
@@ -9,7 +9,6 @@  {-# LANGUAGE NamedFieldPuns    #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards   #-}  module Hledger.Data.Transaction ( -- * Transaction@@ -27,6 +26,7 @@ , transactionTransformPostings , transactionApplyValuation , transactionToCost+, transactionAddInferredEquityPostings , transactionApplyAliases , transactionMapPostings , transactionMapPostingAmounts@@ -202,9 +202,15 @@ transactionApplyValuation priceoracle styles periodlast today v =   transactionTransformPostings (postingApplyValuation priceoracle styles periodlast today v) --- | Convert this transaction's amounts to cost, and apply the appropriate amount styles.-transactionToCost :: M.Map CommoditySymbol AmountStyle -> Transaction -> Transaction-transactionToCost styles = transactionTransformPostings (postingToCost styles)+-- | Maybe convert this 'Transaction's amounts to cost and apply the+-- appropriate amount styles.+transactionToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Transaction -> Transaction+transactionToCost styles cost = transactionMapPostings (postingToCost styles cost)++-- | Add inferred equity postings to a 'Transaction' using transaction prices.+transactionAddInferredEquityPostings :: AccountName -> Transaction -> Transaction+transactionAddInferredEquityPostings equityAcct t =+    t{tpostings=concatMap (postingAddInferredEquityPostings equityAcct) $ tpostings t}  -- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases. -- This can fail due to a bad replacement pattern in a regular expression alias.
Hledger/Data/TransactionModifier.hs view
@@ -18,12 +18,12 @@ import qualified Data.Text as T import Data.Time.Calendar (Day) import Hledger.Data.Types-import Hledger.Data.Dates import Hledger.Data.Amount+import Hledger.Data.Dates import Hledger.Data.Transaction (txnTieKnot)-import Hledger.Query (Query, filterQuery, matchesAmount, matchesPosting,+import Hledger.Query (Query, filterQuery, matchesAmount, matchesPostingExtra,                       parseQuery, queryIsAmt, queryIsSym, simplifyQuery)-import Hledger.Data.Posting (commentJoin, commentAddTag, postingApplyCommodityStyles)+import Hledger.Data.Posting (commentJoin, commentAddTag, postingAddTags, postingApplyCommodityStyles) import Hledger.Utils (dbg6, wrap)  -- $setup@@ -36,9 +36,13 @@ -- Or if any of them fails to be parsed, return the first error. A reference -- date is provided to help interpret relative dates in transaction modifier -- queries.-modifyTransactions :: M.Map CommoditySymbol AmountStyle -> Day -> [TransactionModifier] -> [Transaction] -> Either String [Transaction]-modifyTransactions styles d tmods ts = do-  fs <- mapM (transactionModifierToFunction styles d) tmods  -- convert modifiers to functions, or return a parse error+modifyTransactions :: (AccountName -> Maybe AccountType)+                   -> (AccountName -> [Tag])+                   -> M.Map CommoditySymbol AmountStyle+                   -> Day -> [TransactionModifier] -> [Transaction]+                   -> Either String [Transaction]+modifyTransactions atypes atags styles d tmods ts = do+  fs <- mapM (transactionModifierToFunction atypes atags styles d) tmods  -- convert modifiers to functions, or return a parse error   let     modifytxn t = t''       where@@ -62,7 +66,7 @@ -- >>> import qualified Data.Text.IO as T -- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]} -- >>> tmpost acc amt = TMPostingRule (acc `post` amt) False--- >>> test = either putStr (T.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction mempty nulldate+-- >>> test = either putStr (T.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction (const Nothing) (const []) mempty nulldate -- >>> test $ TransactionModifier "" ["pong" `tmpost` usd 2] -- 0000-01-01 --     ping           $1.00@@ -78,13 +82,18 @@ --     pong           $3.00  ; generated-posting: = ping -- <BLANKLINE> ---transactionModifierToFunction :: M.Map CommoditySymbol AmountStyle -> Day -> TransactionModifier -> Either String (Transaction -> Transaction)-transactionModifierToFunction styles refdate TransactionModifier{tmquerytxt, tmpostingrules} = do+transactionModifierToFunction :: (AccountName -> Maybe AccountType)+                              -> (AccountName -> [Tag])+                              -> M.Map CommoditySymbol AmountStyle+                              -> Day -> TransactionModifier+                              -> Either String (Transaction -> Transaction)+transactionModifierToFunction atypes atags styles refdate TransactionModifier{tmquerytxt, tmpostingrules} = do   q <- simplifyQuery . fst <$> parseQuery refdate tmquerytxt   let-    fs = map (tmPostingRuleToFunction styles q tmquerytxt) tmpostingrules-    generatePostings = concatMap (\p -> p : map ($ p) (if q `matchesPosting` p then fs else []))-  Right $ \t@(tpostings -> ps) -> txnTieKnot t{tpostings=generatePostings ps}+    fs = map (\tmpr -> addAccountTags . tmPostingRuleToFunction styles q tmquerytxt tmpr) tmpostingrules+    addAccountTags p = p `postingAddTags` atags (paccount p)+    generatePostings p = p : map ($ p) (if matchesPostingExtra atypes q p then fs else [])+  Right $ \t@(tpostings -> ps) -> txnTieKnot t{tpostings=concatMap generatePostings ps}  -- | Converts a 'TransactionModifier''s posting rule to a 'Posting'-generating function, -- which will be used to make a new posting based on the old one (an "automated posting").
Hledger/Data/Types.hs view
@@ -84,10 +84,9 @@   = SmartAssumeStart Year (Maybe (Month, Maybe MonthDay))   | SmartFromReference (Maybe Month) MonthDay   | SmartMonth Month-  | SmartRelative SmartSequence SmartInterval+  | SmartRelative Integer SmartInterval   deriving (Show) -data SmartSequence = Last | This | Next deriving (Show) data SmartInterval = Day | Week | Month | Quarter | Year deriving (Show)  data WhichDate = PrimaryDate | SecondaryDate deriving (Eq,Show)@@ -151,8 +150,32 @@   | Revenue   | Expense   | Cash  -- ^ a subtype of Asset - liquid assets to show in cashflow report-  deriving (Show,Eq,Ord,Generic)+  | Conversion -- ^ a subtype of Equity - account in which to generate conversion postings for transaction prices+  deriving (Eq,Ord,Generic) +instance Show AccountType where+  show Asset      = "A"+  show Liability  = "L"+  show Equity     = "E"+  show Revenue    = "R"+  show Expense    = "X"+  show Cash       = "C"+  show Conversion = "V"++-- | Check whether the first argument is a subtype of the second: either equal+-- or one of the defined subtypes.+isAccountSubtypeOf :: AccountType -> AccountType -> Bool+isAccountSubtypeOf Asset      Asset      = True+isAccountSubtypeOf Liability  Liability  = True+isAccountSubtypeOf Equity     Equity     = True+isAccountSubtypeOf Revenue    Revenue    = True+isAccountSubtypeOf Expense    Expense    = True+isAccountSubtypeOf Cash       Cash       = True+isAccountSubtypeOf Cash       Asset      = True+isAccountSubtypeOf Conversion Conversion = True+isAccountSubtypeOf Conversion Equity     = True+isAccountSubtypeOf _          _          = False+ -- not worth the trouble, letters defined in accountdirectivep for now --instance Read AccountType --  where@@ -362,7 +385,8 @@       pamount           :: MixedAmount,       pcomment          :: Text,              -- ^ this posting's comment lines, as a single non-indented multi-line string       ptype             :: PostingType,-      ptags             :: [Tag],                   -- ^ tag names and values, extracted from the comment+      ptags             :: [Tag],                   -- ^ tag names and values, extracted from the posting comment +                                                    --   and (after finalisation) the posting account's directive if any       pbalanceassertion :: Maybe BalanceAssertion,  -- ^ an expected balance in the account after this posting,                                                     --   in a single commodity, excluding subaccounts.       ptransaction      :: Maybe Transaction,       -- ^ this posting's parent transaction (co-recursive types).@@ -512,7 +536,9 @@   -- principal data   ,jdeclaredpayees        :: [(Payee,PayeeDeclarationInfo)]         -- ^ Payees declared by payee directives, in parse order (after journal finalisation)   ,jdeclaredaccounts      :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order (after journal finalisation)-  ,jdeclaredaccounttypes  :: M.Map AccountType [AccountName]        -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts)+  ,jdeclaredaccounttags   :: M.Map AccountName [Tag]                -- ^ Accounts which have tags declared in their directives, and those tags. (Does not include parents' tags.)+  ,jdeclaredaccounttypes  :: M.Map AccountType [AccountName]        -- ^ Accounts whose type has been explicitly declared in their account directives, grouped by type.+  ,jaccounttypes          :: M.Map AccountName AccountType          -- ^ All accounts for which a type has been declared or can be inferred from its parent or its name.   ,jglobalcommoditystyles :: M.Map CommoditySymbol AmountStyle      -- ^ per-commodity display styles declared globally, eg by command line option or import command   ,jcommodities           :: M.Map CommoditySymbol Commodity        -- ^ commodities and formats declared by commodity directives   ,jinferredcommodities   :: M.Map CommoditySymbol AmountStyle      -- ^ commodities and formats inferred from journal amounts@@ -528,6 +554,8 @@                                                                     --   TODO: FilePath is a sloppy type here, don't assume it's a                                                                     --   real file; values like "", "-", "(string)" can be seen   ,jlastreadtime          :: POSIXTime                              -- ^ when this journal was last read from its file(s)+  -- NOTE: after adding new fields, eg involving account names, consider updating+  -- the Anon instance in Hleger.Cli.Anon   } deriving (Eq, Generic)  -- | A journal in the process of being parsed, not yet finalised.
Hledger/Data/Valuation.hs view
@@ -13,7 +13,7 @@ {-# LANGUAGE DeriveGeneric #-}  module Hledger.Data.Valuation (-   Costing(..)+   ConversionOp(..)   ,ValuationType(..)   ,PriceOracle   ,journalPriceOracle@@ -51,8 +51,10 @@ ------------------------------------------------------------------------------ -- Types --- | Whether to convert amounts to cost.-data Costing = Cost | NoCost+-- | Which operation to perform on conversion transactions.+-- (There was also an "infer equity postings" operation, but that is now done +-- earlier, in journal finalisation.)+data ConversionOp = NoConversionOp | ToCost   deriving (Show,Eq)  -- | What kind of value conversion should be done on amounts ?@@ -98,8 +100,8 @@ -- Converting things to value  -- | Convert all component amounts to cost/selling price if requested, and style them.-mixedAmountToCost :: Costing -> M.Map CommoditySymbol AmountStyle -> MixedAmount -> MixedAmount-mixedAmountToCost cost styles = mapMixedAmount (amountToCost cost styles)+mixedAmountToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> MixedAmount -> MixedAmount+mixedAmountToCost styles cost = mapMixedAmount (amountToCost styles cost)  -- | Apply a specified valuation to this mixed amount, using the -- provided price oracle, commodity styles, and reference dates.@@ -109,9 +111,9 @@   mapMixedAmount (amountApplyValuation priceoracle styles periodlast today postingdate v)  -- | Convert an Amount to its cost if requested, and style it appropriately.-amountToCost :: Costing -> M.Map CommoditySymbol AmountStyle -> Amount -> Amount-amountToCost NoCost _      = id-amountToCost Cost   styles = styleAmount styles . amountCost+amountToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Amount -> Amount+amountToCost styles ToCost         = styleAmount styles . amountCost+amountToCost _      NoConversionOp = id  -- | Apply a specified valuation to this amount, using the provided -- price oracle, and reference dates. Also fix up its display style
Hledger/Query.hs view
@@ -21,6 +21,7 @@   parseQuery,   parseQueryList,   parseQueryTerm,+  parseAccountType,   simplifyQuery,   filterQuery,   filterQueryOrNotQuery,@@ -36,6 +37,8 @@   queryIsSym,   queryIsReal,   queryIsStatus,+  queryIsType,+  queryIsTag,   queryStartDate,   queryEndDate,   queryDateSpan,@@ -45,10 +48,13 @@   inAccountQuery,   -- * matching   matchesTransaction,+  matchesTransactionExtra,   matchesDescription,   matchesPayeeWIP,   matchesPosting,+  matchesPostingExtra,   matchesAccount,+  matchesAccountExtra,   matchesMixedAmount,   matchesAmount,   matchesCommodity,@@ -64,7 +70,7 @@ import Control.Applicative ((<|>), many, optional) import Data.Default (Default(..)) import Data.Either (fromLeft, partitionEithers)-import Data.List (partition)+import Data.List (partition, intercalate) import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Text (Text) import qualified Data.Text as T@@ -103,6 +109,7 @@                               --   and sometimes like a query option (for controlling display)            | Tag Regexp (Maybe Regexp)  -- ^ match if a tag's name, and optionally its value, is matched by these respective regexps                                         -- matching the regexp if provided, exists+           | Type [AccountType]  -- ^ match accounts whose type is one of these (or with no types, any account)     deriving (Eq,Show)  instance Default Query where def = Any@@ -238,6 +245,7 @@     ,"empty"     ,"depth"     ,"tag"+    ,"type"     ]  defaultprefix :: T.Text@@ -286,6 +294,7 @@  parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = Left . Sym <$> toRegexCI ("^" <> s <> "$") -- support cur: as an alias parseQueryTerm _ (T.stripPrefix "tag:" -> Just s) = Left <$> parseTag s+parseQueryTerm _ (T.stripPrefix "type:" -> Just s) = Left <$> parseTypeCodes s parseQueryTerm _ "" = Right $ Left $ Any parseQueryTerm d s = parseQueryTerm d $ defaultprefix<>":"<>s @@ -339,6 +348,45 @@     return $ Tag tag body   where (n,v) = T.break (=='=') s +-- | Parse one or more account type code letters to a query matching any of those types.+parseTypeCodes :: T.Text -> Either String Query+parseTypeCodes s =+  case partitionEithers $ map (parseAccountType False . T.singleton) $ T.unpack s of+    ((e:_),_) -> Left $ "could not parse " <> show e <> " as an account type code.\n" <> help+    ([],[])   -> Left help+    ([],ts)   -> Right $ Type ts+  where+    help = "type:'s argument should be one or more of " ++ accountTypeChoices False ++ " (case insensitive)."++accountTypeChoices :: Bool -> String+accountTypeChoices allowlongform = +  intercalate ", " +    -- keep synced with parseAccountType+    $ ["A","L","E","R","X","C","V"]+    ++ if allowlongform then ["Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"] else []++-- | Case-insensitively parse one single-letter code, or one long-form word if permitted, to an account type.+-- On failure, returns the unparseable text.+parseAccountType :: Bool -> Text -> Either String AccountType+parseAccountType allowlongform s =+  case T.toLower s of+    -- keep synced with accountTypeChoices+    "a"                          -> Right Asset+    "l"                          -> Right Liability+    "e"                          -> Right Equity+    "r"                          -> Right Revenue+    "x"                          -> Right Expense+    "c"                          -> Right Cash+    "v"                          -> Right Conversion+    "asset"      | allowlongform -> Right Asset+    "liability"  | allowlongform -> Right Liability+    "equity"     | allowlongform -> Right Equity+    "revenue"    | allowlongform -> Right Revenue+    "expense"    | allowlongform -> Right Expense+    "cash"       | allowlongform -> Right Cash+    "conversion" | allowlongform -> Right Conversion+    _                            -> Left $ T.unpack s+ -- | Parse the value part of a "status:" query, or return an error. parseStatus :: T.Text -> Either String Status parseStatus s | s `elem` ["*","1"] = Right Cleared@@ -457,6 +505,14 @@ queryIsStatus (StatusQ _) = True queryIsStatus _ = False +queryIsType :: Query -> Bool+queryIsType (Type _) = True+queryIsType _ = False++queryIsTag :: Query -> Bool+queryIsTag (Tag _ _) = True+queryIsTag _ = False+ -- | Does this query specify a start date and nothing else (that would -- filter postings prior to the date) ? -- When the flag is true, look for a starting secondary date instead.@@ -562,25 +618,6 @@  -- matching --- | Does the match expression match this account ?--- A matching in: clause is also considered a match.--- When matching by account name pattern, if there's a regular--- expression error, this function calls error.-matchesAccount :: Query -> AccountName -> Bool-matchesAccount (None) _ = False-matchesAccount (Not m) a = not $ matchesAccount m a-matchesAccount (Or ms) a = any (`matchesAccount` a) ms-matchesAccount (And ms) a = all (`matchesAccount` a) ms-matchesAccount (Acct r) a = regexMatchText r a-matchesAccount (Depth d) a = accountNameLevel a <= d-matchesAccount (Tag _ _) _ = False-matchesAccount _ _ = True--matchesMixedAmount :: Query -> MixedAmount -> Bool-matchesMixedAmount q ma = case amountsRaw ma of-    [] -> q `matchesAmount` nullamt-    as -> any (q `matchesAmount`) as- matchesCommodity :: Query -> CommoditySymbol -> Bool matchesCommodity (Sym r) = regexMatchText r matchesCommodity _ = const True@@ -596,9 +633,6 @@ matchesAmount (Sym r) a = matchesCommodity (Sym r) (acommodity a) matchesAmount _ _ = True --- | Is this simple (single-amount) mixed amount's quantity less than, greater than, equal to, or unsignedly equal to this number ?--- For multi-amount (multiple commodities, or just unsimplified) mixed amounts this is always true.- -- | Is this amount's quantity less than, greater than, equal to, or unsignedly equal to this number ? compareAmount :: OrdPlus -> Quantity -> Amount -> Bool compareAmount ord q Amount{aquantity=aq} = case ord of Lt      -> aq <  q@@ -612,9 +646,42 @@                                                        AbsGtEq -> abs aq >= abs q                                                        AbsEq   -> abs aq == abs q --- | Does the match expression match this posting ?+matchesMixedAmount :: Query -> MixedAmount -> Bool+matchesMixedAmount q ma = case amountsRaw ma of+    [] -> q `matchesAmount` nullamt+    as -> any (q `matchesAmount`) as++-- | Does the query match this account name ?+-- A matching in: clause is also considered a match.+matchesAccount :: Query -> AccountName -> Bool+matchesAccount (None) _ = False+matchesAccount (Not m) a = not $ matchesAccount m a+matchesAccount (Or ms) a = any (`matchesAccount` a) ms+matchesAccount (And ms) a = all (`matchesAccount` a) ms+matchesAccount (Acct r) a = regexMatchText r a+matchesAccount (Depth d) a = accountNameLevel a <= d+matchesAccount (Tag _ _) _ = False+matchesAccount _ _ = True++-- | Like matchesAccount, but with optional extra matching features: ----- Note that for account match we try both original and effective account+-- - If the account's type is provided, any type: terms in the query+--   must match it (and any negated type: terms must not match it).+--+-- - If the account's tags are provided, any tag: terms must match+--   at least one of them (and any negated tag: terms must match none).+--+matchesAccountExtra :: (AccountName -> Maybe AccountType) -> (AccountName -> [Tag]) -> Query -> AccountName -> Bool+matchesAccountExtra atypes atags (Not q  ) a = not $ matchesAccountExtra atypes atags q a+matchesAccountExtra atypes atags (Or  qs ) a = any (\q -> matchesAccountExtra atypes atags q a) qs+matchesAccountExtra atypes atags (And qs ) a = all (\q -> matchesAccountExtra atypes atags q a) qs+matchesAccountExtra atypes _     (Type ts) a = maybe False (\t -> any (t `isAccountSubtypeOf`) ts) $ atypes a+matchesAccountExtra _      atags (Tag npat vpat) a = matchesTags npat vpat $ atags a+matchesAccountExtra _      _     q         a = matchesAccount q a++-- | Does the match expression match this posting ?+-- When matching account name, and the posting has been transformed+-- in some way, we will match either the original or transformed name. matchesPosting :: Query -> Posting -> Bool matchesPosting (Not q) p = not $ q `matchesPosting` p matchesPosting (Any) _ = True@@ -623,8 +690,7 @@ matchesPosting (And qs) p = all (`matchesPosting` p) qs matchesPosting (Code r) p = maybe False (regexMatchText r . tcode) $ ptransaction p matchesPosting (Desc r) p = maybe False (regexMatchText r . tdescription) $ ptransaction p-matchesPosting (Acct r) p = matches p || maybe False matches (poriginal p)-  where matches = regexMatchText r . paccount+matchesPosting (Acct r) p = matches p || maybe False matches (poriginal p) where matches = regexMatchText r . paccount matchesPosting (Date span) p = span `spanContainsDate` postingDate p matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p matchesPosting (StatusQ s) p = postingStatus p == s@@ -635,8 +701,19 @@ matchesPosting (Tag n v) p = case (reString n, v) of   ("payee", Just v) -> maybe False (regexMatchText v . transactionPayee) $ ptransaction p   ("note", Just v) -> maybe False (regexMatchText v . transactionNote) $ ptransaction p-  (_, v) -> matchesTags n v $ postingAllTags p+  (_, mv) -> matchesTags n mv $ postingAllTags p+matchesPosting (Type _) _ = False +-- | Like matchesPosting, but if the posting's account's type is provided,+-- any type: terms in the query must match it (and any negated type: terms+-- must not match it).+matchesPostingExtra :: (AccountName -> Maybe AccountType) -> Query -> Posting -> Bool+matchesPostingExtra atype (Not q )  p = not $ matchesPostingExtra atype q p+matchesPostingExtra atype (Or  qs)  p = any (\q -> matchesPostingExtra atype q p) qs+matchesPostingExtra atype (And qs)  p = all (\q -> matchesPostingExtra atype q p) qs+matchesPostingExtra atype (Type ts) p = maybe False (\t -> any (t `isAccountSubtypeOf`) ts) . atype $ paccount p+matchesPostingExtra _ q p             = matchesPosting q p+ -- | Does the match expression match this transaction ? matchesTransaction :: Query -> Transaction -> Bool matchesTransaction (Not q) t = not $ q `matchesTransaction` t@@ -658,7 +735,18 @@   ("payee", Just v) -> regexMatchText v $ transactionPayee t   ("note", Just v) -> regexMatchText v $ transactionNote t   (_, v) -> matchesTags n v $ transactionAllTags t+matchesTransaction (Type _) _ = False +-- | Like matchesTransaction, but if the journal's account types are provided,+-- any type: terms in the query must match at least one posting's account type+-- (and any negated type: terms must match none).+matchesTransactionExtra :: (AccountName -> Maybe AccountType) -> Query -> Transaction -> Bool+matchesTransactionExtra atype (Not  q) t = not $ matchesTransactionExtra atype q t+matchesTransactionExtra atype (Or  qs) t = any (\q -> matchesTransactionExtra atype q t) qs+matchesTransactionExtra atype (And qs) t = all (\q -> matchesTransactionExtra atype q t) qs+matchesTransactionExtra atype q@(Type _) t = any (matchesPostingExtra atype q) $ tpostings t+matchesTransactionExtra _ q t = matchesTransaction q t+ -- | Does the query match this transaction description ? -- Tests desc: terms, any other terms are ignored. matchesDescription :: Query -> Text -> Bool@@ -669,15 +757,7 @@ matchesDescription (And qs) d     = all (`matchesDescription` d) $ filter queryIsDesc qs matchesDescription (Code _) _     = False matchesDescription (Desc r) d     = regexMatchText r d-matchesDescription (Acct _) _     = False-matchesDescription (Date _) _     = False-matchesDescription (Date2 _) _    = False-matchesDescription (StatusQ _) _  = False-matchesDescription (Real _) _     = False-matchesDescription (Amt _ _) _    = False-matchesDescription (Depth _) _    = False-matchesDescription (Sym _) _      = False-matchesDescription (Tag _ _) _    = False+matchesDescription _ _            = False  -- | Does the query match this transaction payee ? -- Tests desc: (and payee: ?) terms, any other terms are ignored.@@ -800,6 +880,11 @@      assertBool "" $ Date nulldatespan `matchesAccount` "a"      assertBool "" $ Date2 nulldatespan `matchesAccount` "a"      assertBool "" $ not $ Tag (toRegex' "a") Nothing `matchesAccount` "a"++  ,testCase "matchesAccountExtra" $ do+     let tagq = Tag (toRegexCI' "type") Nothing+     assertBool "" $ not $ matchesAccountExtra (const Nothing) (const []) tagq "a"+     assertBool "" $       matchesAccountExtra (const Nothing) (const [("type","")]) tagq "a"    ,testGroup "matchesPosting" [      testCase "positive match on cleared posting status"  $
Hledger/Read/Common.hs view
@@ -19,7 +19,6 @@ {-# LANGUAGE NamedFieldPuns      #-} {-# LANGUAGE NoMonoLocalBinds    #-} {-# LANGUAGE OverloadedStrings   #-}-{-# LANGUAGE PackageImports      #-} {-# LANGUAGE Rank2Types          #-} {-# LANGUAGE RecordWildCards     #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -49,6 +48,7 @@   getDefaultCommodityAndStyle,   getDefaultAmountStyle,   getAmountStyle,+  addDeclaredAccountTags,   addDeclaredAccountType,   pushParentAccount,   popParentAccount,@@ -129,7 +129,7 @@ import Data.Either (lefts, rights) import Data.Function ((&)) import Data.Functor ((<&>), ($>))-import Data.List (find, genericReplicate)+import Data.List (find, genericReplicate, union) import Data.List.NonEmpty (NonEmpty(..)) import Data.Maybe (catMaybes, fromMaybe, isJust, listToMaybe) import qualified Data.Map as M@@ -213,6 +213,7 @@       ,forecast_          = forecastPeriodFromRawOpts day rawopts       ,reportspan_        = DateSpan (queryStartDate False datequery) (queryEndDate False datequery)       ,auto_              = boolopt "auto" rawopts+      ,infer_equity_      = boolopt "infer-equity" rawopts && conversionop_ ropts /= Just ToCost       ,balancingopts_     = defbalancingopts{                                  ignore_assertions_ = boolopt "ignore-assertions" rawopts                                , infer_transaction_prices_ = not noinferprice@@ -287,47 +288,58 @@  -- | Post-process a Journal that has just been parsed or generated, in this order: ----- - apply canonical amount styles,+-- - add misc info (file path, read time)  ----- - save misc info and reverse transactions into their original parse order,+-- - reverse transactions into their original parse order ----- - add forecast transactions,+-- - apply canonical commodity styles ----- - evaluate balance assignments and balance each transaction,+-- - add tags from account directives to postings' tags ----- - apply transaction modifiers (auto postings) if enabled,+-- - add forecast transactions if enabled ----- - check balance assertions if enabled.+-- - add tags from account directives to postings' tags (again to affect forecast transactions) ----- - infer transaction-implied market prices from transaction prices+-- - add auto postings if enabled --+-- - add tags from account directives to postings' tags (again to affect auto postings)+--+-- - evaluate balance assignments and balance each transaction+--+-- - check balance assertions if enabled+--+-- - infer equity postings in conversion transactions if enabled+--+-- - infer market prices from costs if enabled+--+-- - check all accounts have been declared if in strict mode+--+-- - check all commodities have been declared if in strict mode+-- journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal-journalFinalise iopts@InputOpts{auto_,balancingopts_,strict_} f txt pj = do-    t <- liftIO getPOSIXTime-    -- Infer and apply canonical styles for each commodity (or throw an error).-    -- This affects transaction balancing/assertions/assignments, so needs to be done early.-    liftEither $ checkAddAndBalance (_ioDay iopts) <=< journalApplyCommodityStyles $-        pj{jglobalcommoditystyles=fromMaybe mempty $ commodity_styles_ balancingopts_}  -- save any global commodity styles-        & journalAddFile (f, txt)           -- save the main file's info-        & journalSetLastReadTime t          -- save the last read time-        & journalReverse                    -- convert all lists to the order they were parsed-  where-    checkAddAndBalance d j = do-        when strict_ $ do-          -- If in strict mode, check all postings are to declared accounts-          journalCheckAccountsDeclared j-          -- and using declared commodities-          journalCheckCommoditiesDeclared j--        -- Add forecast transactions if enabled-        journalAddForecast (forecastPeriod iopts j) j-        -- Add auto postings if enabled-          & (if auto_ && not (null $ jtxnmodifiers j) then journalAddAutoPostings d balancingopts_ else pure)-        -- Balance all transactions and maybe check balance assertions.-          >>= journalBalanceTransactions balancingopts_-        -- infer market prices from commodity-exchanging transactions-          <&> journalInferMarketPricesFromTransactions+journalFinalise iopts@InputOpts{auto_,infer_equity_,balancingopts_,strict_,_ioDay} f txt pj = do+  t <- liftIO getPOSIXTime+  liftEither $ do+    j <- pj{jglobalcommoditystyles=fromMaybe mempty $ commodity_styles_ balancingopts_}+      &   journalSetLastReadTime t                       -- save the last read time+      &   journalAddFile (f, txt)                        -- save the main file's info+      &   journalReverse                                 -- convert all lists to the order they were parsed+      &   journalAddAccountTypes                         -- build a map of all known account types+      &   journalApplyCommodityStyles                    -- Infer and apply commodity styles - should be done early+      <&> journalAddForecast (forecastPeriod iopts pj)   -- Add forecast transactions if enabled+      <&> journalPostingsAddAccountTags                  -- Add account tags to postings, so they can be matched by auto postings.+      >>= (if auto_ && not (null $ jtxnmodifiers pj)+              then journalAddAutoPostings _ioDay balancingopts_  -- Add auto postings if enabled, and account tags if needed+              else pure)+      >>= journalBalanceTransactions balancingopts_      -- Balance all transactions and maybe check balance assertions.+      <&> (if infer_equity_ then journalAddInferredEquityPostings else id)  -- Add inferred equity postings, after balancing transactions and generating auto postings+      <&> journalInferMarketPricesFromTransactions       -- infer market prices from commodity-exchanging transactions+    when strict_ $ do+      journalCheckAccountsDeclared j                     -- If in strict mode, check all postings are to declared accounts+      journalCheckCommoditiesDeclared j                  -- and using declared commodities+    return j +-- | Apply any auto posting rules to generate extra postings on this journal's transactions. journalAddAutoPostings :: Day -> BalancingOpts -> Journal -> Either String Journal journalAddAutoPostings d bopts =     -- Balance all transactions without checking balance assertions,@@ -403,9 +415,14 @@                           (linesPrepend "  " . (<>"\n") . textChomp $ showTransaction t)       where         mfirstundeclaredcomm =-          find (`M.notMember` jcommodities j) . map acommodity $-          (maybe id ((:) . baamount) pbalanceassertion) . filter (/= missingamt) $ amountsRaw pamount+          find (`M.notMember` jcommodities j)+          . map acommodity+          . (maybe id ((:) . baamount) pbalanceassertion)+          . filter (not . isIgnorable)+          $ amountsRaw pamount +    -- Ignore missing amounts and zero amounts without commodity (#1767)+    isIgnorable a = (T.null (acommodity a) && amountIsZero a) || a == missingamt  setYear :: Year -> JournalParser m () setYear y = modify' (\j -> j{jparsedefaultyear=Just y})@@ -444,6 +461,10 @@   mdefaultStyle <- fmap snd <$> getDefaultCommodityAndStyle   return $ listToMaybe $ catMaybes [mspecificStyle, mdefaultStyle] +addDeclaredAccountTags :: AccountName -> [Tag] -> JournalParser m ()+addDeclaredAccountTags acct atags =+  modify' (\j -> j{jdeclaredaccounttags = M.insertWith (flip union) acct atags (jdeclaredaccounttags j)})+ addDeclaredAccountType :: AccountName -> AccountType -> JournalParser m () addDeclaredAccountType acct atype =   modify' (\j -> j{jdeclaredaccounttypes = M.insertWith (++) atype [acct] (jdeclaredaccounttypes j)})@@ -1575,3 +1596,5 @@     ]    ]++
Hledger/Read/CsvReader.hs view
@@ -45,7 +45,7 @@ import Control.Monad.IO.Class     (MonadIO, liftIO) import Control.Monad.State.Strict (StateT, get, modify', evalStateT) import Control.Monad.Trans.Class  (lift)-import Data.Char                  (toLower, isDigit, isSpace, isAlphaNum, isAscii, ord)+import Data.Char                  (toLower, isDigit, isSpace, isAlphaNum, ord) import Data.Bifunctor             (first) import Data.List (elemIndex, foldl', intersperse, mapAccumL, nub, sortBy) import Data.Maybe (catMaybes, fromMaybe, isJust)@@ -1245,7 +1245,7 @@     t   where     referencep = liftA2 T.cons (char '%') (takeWhile1P (Just "reference") isFieldNameChar) :: Parsec CustomErr Text Text-    isFieldNameChar c = isAscii c && (isAlphaNum c || c == '_' || c == '-')+    isFieldNameChar c = isAlphaNum c || c == '_' || c == '-'  -- | Replace something that looks like a reference to a csv field ("%date" or "%1) -- with that field's value. If it doesn't look like a field reference, or if we
Hledger/Read/InputOptions.hs view
@@ -36,6 +36,7 @@     ,forecast_          :: Maybe DateSpan       -- ^ span in which to generate forecast transactions     ,reportspan_        :: DateSpan             -- ^ a dirty hack keeping the query dates in InputOpts. This rightfully lives in ReportSpec, but is duplicated here.     ,auto_              :: Bool                 -- ^ generate automatic postings when journal is parsed+    ,infer_equity_      :: Bool                 -- ^ generate automatic equity postings from transaction prices     ,balancingopts_     :: BalancingOpts        -- ^ options for balancing transactions     ,strict_            :: Bool                 -- ^ do extra error checking (eg, all posted accounts are declared, no prices are inferred)     ,_ioDay             :: Day                  -- ^ today's date, for use with forecast transactions  XXX this duplicates _rsDay, and should eventually be removed when it's not needed anymore.@@ -53,6 +54,7 @@     , forecast_          = Nothing     , reportspan_        = nulldatespan     , auto_              = False+    , infer_equity_      = False     , balancingopts_     = defbalancingopts     , strict_            = False     , _ioDay             = nulldate
Hledger/Read/JournalReader.hs view
@@ -73,7 +73,7 @@ --- ** imports import qualified Control.Monad.Fail as Fail (fail) import qualified Control.Exception as C-import Control.Monad (forM_, when, void)+import Control.Monad (forM_, when, void, unless) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Except (ExceptT(..), runExceptT) import Control.Monad.State.Strict (evalStateT,get,modify',put)@@ -347,12 +347,6 @@   -- the account name, possibly modified by preceding alias or apply account directives   acct <- modifiedaccountnamep -  -- maybe an account type code (ALERX) after two or more spaces-  -- XXX added in 1.11, deprecated in 1.13, remove in 1.14-  mtypecode :: Maybe Char <- lift $ optional $ try $ do-    skipNonNewlineSpaces1 -- at least one more space in addition to the one consumed by modifiedaccountp-    choice $ map char "ALERX"-   -- maybe a comment, on this and/or following lines   (cmt, tags) <- lift transactioncommentp @@ -362,11 +356,11 @@   -- an account type may have been set by account type code or a tag;   -- the latter takes precedence   let-    mtypecode' :: Maybe Text = lookup accountTypeTagName tags <|> (T.singleton <$> mtypecode)-    metype = parseAccountTypeCode <$> mtypecode'+    metype = parseAccountTypeCode <$> lookup accountTypeTagName tags    -- update the journal   addAccountDeclaration (acct, cmt, tags)+  unless (null tags) $ addDeclaredAccountTags acct tags   case metype of     Nothing         -> return ()     Just (Right t)  -> addDeclaredAccountType acct t@@ -378,22 +372,24 @@ parseAccountTypeCode :: Text -> Either String AccountType parseAccountTypeCode s =   case T.toLower s of-    "asset"     -> Right Asset-    "a"         -> Right Asset-    "liability" -> Right Liability-    "l"         -> Right Liability-    "equity"    -> Right Equity-    "e"         -> Right Equity-    "revenue"   -> Right Revenue-    "r"         -> Right Revenue-    "expense"   -> Right Expense-    "x"         -> Right Expense-    "cash"      -> Right Cash-    "c"         -> Right Cash-    _           -> Left err+    "asset"      -> Right Asset+    "a"          -> Right Asset+    "liability"  -> Right Liability+    "l"          -> Right Liability+    "equity"     -> Right Equity+    "e"          -> Right Equity+    "revenue"    -> Right Revenue+    "r"          -> Right Revenue+    "expense"    -> Right Expense+    "x"          -> Right Expense+    "cash"       -> Right Cash+    "c"          -> Right Cash+    "conversion" -> Right Conversion+    "v"          -> Right Conversion+    _            -> Left err   where     err = T.unpack $ "invalid account type code "<>s<>", should be one of " <>-            T.intercalate ", " ["A","L","E","R","X","C","Asset","Liability","Equity","Revenue","Expense","Cash"]+            T.intercalate ", " ["A","L","E","R","X","C","V","Asset","Liability","Equity","Revenue","Expense","Cash","Conversion"]  -- Add an account declaration to the journal, auto-numbering it. addAccountDeclaration :: (AccountName,Text,[Tag]) -> JournalParser m ()@@ -1008,7 +1004,7 @@   ,testGroup "accountdirectivep" [        testCase "with-comment"       $ assertParse accountdirectivep "account a:b  ; a comment\n"       ,testCase "does-not-support-!" $ assertParseError accountdirectivep "!account a:b\n" ""-      ,testCase "account-type-code"  $ assertParse accountdirectivep "account a:b  A\n"+      ,testCase "account-type-code"  $ assertParse accountdirectivep "account a:b  ; type:A\n"       ,testCase "account-type-tag"   $ assertParseStateOn accountdirectivep "account a:b  ; type:asset\n"         jdeclaredaccounts         [("a:b", AccountDeclarationInfo{adicomment          = "type:asset\n"
Hledger/Reports/BalanceReport.hs view
@@ -313,7 +313,7 @@                 ,"  a:b          10h @ $50"                 ,"  c:d                   "                 ]) >>= either error' return-         let j' = journalCanonicaliseAmounts $ journalToCost j -- enable cost basis adjustment+         let j' = journalCanonicaliseAmounts $ journalToCost ToCost j -- enable cost basis adjustment          balanceReportAsText defreportopts (balanceReport defreportopts Any j') `is`            ["                $500  a:b"            ,"               $-500  c:d"
Hledger/Reports/BudgetReport.hs view
@@ -82,10 +82,12 @@       jperiodictxns j     actualj = journalWithBudgetAccountNames budgetedaccts showunbudgeted j     budgetj = journalAddBudgetGoalTransactions bopts ropts reportspan j-    actualreport@(PeriodicReport actualspans _ _) =-        dbg5 "actualreport" $ multiBalanceReport rspec{_rsReportOpts=ropts{empty_=True}} actualj+    priceoracle = journalPriceOracle (infer_prices_ ropts) j     budgetgoalreport@(PeriodicReport _ budgetgoalitems budgetgoaltotals) =-        dbg5 "budgetgoalreport" $ multiBalanceReport rspec{_rsReportOpts=ropts{empty_=True}} budgetj+        dbg5 "budgetgoalreport" $ multiBalanceReportWith rspec{_rsReportOpts=ropts{empty_=True}} budgetj priceoracle mempty+    budgetedacctsseen = S.fromList $ map prrFullName budgetgoalitems+    actualreport@(PeriodicReport actualspans _ _) =+        dbg5 "actualreport"     $ multiBalanceReportWith rspec{_rsReportOpts=ropts{empty_=True}} actualj priceoracle budgetedacctsseen     budgetgoalreport'       -- If no interval is specified:       -- budgetgoalreport's span might be shorter actualreport's due to periodic txns;@@ -218,9 +220,9 @@       balanceReportTableAsText ropts (budgetReportAsTable ropts budgetr)   where     title = "Budget performance in " <> showDateSpan (periodicReportSpan budgetr)-           <> (case cost_ of-                 Cost   -> ", converted to cost"-                 NoCost -> "")+           <> (case conversionop_ of+                 Just ToCost -> ", converted to cost"+                 _           -> "")            <> (case value_ of                  Just (AtThen _mc)   -> ", valued at posting date"                  Just (AtEnd _mc)    -> ", valued at period ends"@@ -386,9 +388,9 @@         _   -> -- trace (pshow $ (maybecost actual, maybecost budget))  -- debug missing percentage                Nothing       where-        costedAmounts = case cost_ of-            Cost   -> amounts . mixedAmountCost-            NoCost -> amounts+        costedAmounts = case conversionop_ of+            Just ToCost -> amounts . mixedAmountCost+            _           -> amounts      -- | Calculate the percentage of actual change to budget goal for a particular commodity     percentage' :: Change -> BudgetGoal -> CommoditySymbol -> Maybe Percentage
Hledger/Reports/EntriesReport.hs view
@@ -36,7 +36,7 @@ entriesReport :: ReportSpec -> Journal -> EntriesReport entriesReport rspec@ReportSpec{_rsReportOpts=ropts} =     sortBy (comparing $ transactionDateFn ropts) . jtxns-    . journalApplyValuationFromOpts rspec{_rsReportOpts=ropts{show_costs_=True}}+    . journalApplyValuationFromOpts (setDefaultConversionOp NoConversionOp rspec)     . filterJournalTransactions (_rsQuery rspec)  tests_EntriesReport = testGroup "EntriesReport" [
Hledger/Reports/MultiBalanceReport.hs view
@@ -47,6 +47,8 @@ import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Ord (Down(..)) import Data.Semigroup (sconcat)+import Data.Set (Set)+import qualified Data.Set as Set import Data.Time.Calendar (fromGregorian) import Safe (lastDef, minimumMay) @@ -102,23 +104,23 @@ -- by the balance command (in multiperiod mode) and (via compoundBalanceReport) -- by the bs/cf/is commands. multiBalanceReport :: ReportSpec -> Journal -> MultiBalanceReport-multiBalanceReport rspec j = multiBalanceReportWith rspec j (journalPriceOracle infer j)+multiBalanceReport rspec j = multiBalanceReportWith rspec j (journalPriceOracle infer j) mempty   where infer = infer_prices_ $ _rsReportOpts rspec --- | A helper for multiBalanceReport. This one takes an extra argument,--- a PriceOracle to be used for looking up market prices. Commands which--- run multiple reports (bs etc.) can generate the price oracle just--- once for efficiency, passing it to each report by calling this--- function directly.-multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> MultiBalanceReport-multiBalanceReportWith rspec' j priceoracle = report+-- | A helper for multiBalanceReport. This one takes some extra arguments,+-- a 'PriceOracle' to be used for looking up market prices, and a set of+-- 'AccountName's which should not be elided. Commands which run multiple+-- reports (bs etc.) can generate the price oracle just once for efficiency,+-- passing it to each report by calling this function directly.+multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> Set AccountName -> MultiBalanceReport+multiBalanceReportWith rspec' j priceoracle unelidableaccts = report   where     -- Queries, report/column dates.-    reportspan = dbg3 "reportspan" $ reportSpan j rspec'-    rspec      = dbg3 "reportopts" $ makeReportQuery rspec' reportspan+    (reportspan, colspans) = reportSpan j rspec'+    rspec = dbg3 "reportopts" $ makeReportQuery rspec' reportspan      -- Group postings into their columns.-    colps = dbg5 "colps" $ getPostingsByColumn rspec j priceoracle reportspan+    colps = dbg5 "colps" $ getPostingsByColumn rspec j priceoracle colspans      -- The matched accounts with a starting balance. All of these should appear     -- in the report, even if they have no postings during the report period.@@ -127,7 +129,7 @@      -- Generate and postprocess the report, negating balances and taking percentages if needed     report = dbg4 "multiBalanceReportWith" $-      generateMultiBalanceReport rspec j priceoracle colps startbals+      generateMultiBalanceReport rspec j priceoracle unelidableaccts colps startbals  -- | Generate a compound balance report from a list of CBCSubreportSpec. This -- shares postings between the subreports.@@ -143,11 +145,11 @@ compoundBalanceReportWith rspec' j priceoracle subreportspecs = cbr   where     -- Queries, report/column dates.-    reportspan = dbg3 "reportspan" $ reportSpan j rspec'-    rspec      = dbg3 "reportopts" $ makeReportQuery rspec' reportspan+    (reportspan, colspans) = reportSpan j rspec'+    rspec = dbg3 "reportopts" $ makeReportQuery rspec' reportspan      -- Group postings into their columns.-    colps = dbg5 "colps" $ getPostingsByColumn rspec j priceoracle reportspan+    colps = dbg5 "colps" $ getPostingsByColumn rspec j priceoracle colspans      -- The matched postings with a starting balance. All of these should appear     -- in the report, even if they have no postings during the report period.@@ -159,7 +161,7 @@             ( cbcsubreporttitle             -- Postprocess the report, negating balances and taking percentages if needed             , cbcsubreporttransform $-                generateMultiBalanceReport rspecsub j priceoracle colps' startbals'+                generateMultiBalanceReport rspecsub j priceoracle mempty colps' startbals'             , cbcsubreportincreasestotal             )           where@@ -169,8 +171,9 @@             ropts = cbcsubreportoptions $ _rsReportOpts rspec             rspecsub = rspec{_rsReportOpts=ropts, _rsQuery=And [q, _rsQuery rspec]}             -- Starting balances and column postings specific to this subreport.-            startbals' = startingBalances rspecsub j priceoracle $ filter (matchesPosting q) startps-            colps'     = map (second $ filter (matchesPosting q)) colps+            startbals' = startingBalances rspecsub j priceoracle $+              filter (matchesPostingExtra (journalAccountType j) q) startps+            colps' = map (second $ filter (matchesPostingExtra (journalAccountType j) q)) colps      -- Sum the subreport totals by column. Handle these cases:     -- - no subreports@@ -240,14 +243,13 @@     dateqcons        = if date2_ (_rsReportOpts rspec) then Date2 else Date  -- | Group postings, grouped by their column-getPostingsByColumn :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> [(DateSpan, [Posting])]-getPostingsByColumn rspec j priceoracle reportspan =+getPostingsByColumn :: ReportSpec -> Journal -> PriceOracle -> [DateSpan] -> [(DateSpan, [Posting])]+getPostingsByColumn rspec j priceoracle colspans =     groupByDateSpan True getDate colspans ps   where     -- Postings matching the query within the report period.     ps = dbg5 "getPostingsByColumn" $ getPostings rspec j priceoracle     -- The date spans to be included as report columns.-    colspans = dbg3 "colspans" $ splitSpan (interval_ $ _rsReportOpts rspec) reportspan     getDate = postingDateOrDate2 (whichDate (_rsReportOpts rspec))  -- | Gather postings matching the query within the report period.@@ -257,7 +259,7 @@   where     rspec' = rspec{_rsQuery=depthless, _rsReportOpts = ropts'}     ropts' = if isJust (valuationAfterSum ropts)-        then ropts{value_=Nothing, cost_=NoCost}  -- If we're valuing after the sum, don't do it now+        then ropts{value_=Nothing, conversionop_=Just NoConversionOp}  -- If we're valuing after the sum, don't do it now         else ropts      -- The user's query with no depth limit, and expanded to the report span@@ -281,11 +283,15 @@     -- and the declared accounts are really only needed for the former,      -- but it's harmless to have them in the column changes as well.     ps' = ps ++ if declared_ then declaredacctps else []-      where -        declaredacctps = -          [nullposting{paccount=n} | n <- journalLeafAccountNamesDeclared j-                                   , acctq `matchesAccount` n]-          where acctq  = dbg3 "acctq" $ filterQueryOrNotQuery queryIsAcct query+      where+        declaredacctps =+          [nullposting{paccount=a}+          | a <- journalLeafAccountNamesDeclared j+          , matchesAccountExtra (journalAccountType j) (journalAccountTags j) accttypetagsq a+          ]+          where+            accttypetagsq  = dbg3 "accttypetagsq" $+              filterQueryOrNotQuery (\q -> queryIsAcct q || queryIsType q || queryIsTag q) query      filterbydepth = case accountlistmode_ of       ALTree -> filter ((depthq `matchesAccount`) . aname)    -- a tree - just exclude deeper accounts@@ -343,17 +349,17 @@ -- | Lay out a set of postings grouped by date span into a regular matrix with rows -- given by AccountName and columns by DateSpan, then generate a MultiBalanceReport -- from the columns.-generateMultiBalanceReport :: ReportSpec -> Journal -> PriceOracle+generateMultiBalanceReport :: ReportSpec -> Journal -> PriceOracle -> Set AccountName                            -> [(DateSpan, [Posting])] -> HashMap AccountName Account                            -> MultiBalanceReport-generateMultiBalanceReport rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle colps startbals =+generateMultiBalanceReport rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle unelidableaccts colps startbals =     report   where     -- Process changes into normal, cumulative, or historical amounts, plus value them     matrix = calculateReportMatrix rspec j priceoracle startbals colps      -- All account names that will be displayed, possibly depth-clipped.-    displaynames = dbg5 "displaynames" $ displayedAccounts rspec matrix+    displaynames = dbg5 "displaynames" $ displayedAccounts rspec unelidableaccts matrix      -- All the rows of the report.     rows = dbg5 "rows" . (if invert_ ropts then map (fmap maNegate) else id)  -- Negate amounts if applicable@@ -394,9 +400,11 @@  -- | Calculate accounts which are to be displayed in the report, as well as -- their name and depth-displayedAccounts :: ReportSpec -> HashMap AccountName (Map DateSpan Account)+displayedAccounts :: ReportSpec+                  -> Set AccountName+                  -> HashMap AccountName (Map DateSpan Account)                   -> HashMap AccountName DisplayName-displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} valuedaccts+displayedAccounts ReportSpec{_rsQuery=query,_rsReportOpts=ropts} unelidableaccts valuedaccts     | depth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1     | otherwise  = HM.mapWithKey (\a _ -> displayedName a) displayedAccts   where@@ -421,7 +429,8 @@     -- Accounts interesting for their own sake     isInteresting name amts =         d <= depth                                 -- Throw out anything too deep-        && ( (empty_ ropts && keepWhenEmpty amts)  -- Keep empty accounts when called with --empty+        && ( name `Set.member` unelidableaccts     -- Unelidable accounts should be kept unless too deep+           ||(empty_ ropts && keepWhenEmpty amts)  -- Keep empty accounts when called with --empty            || not (isZeroRow balance amts)         -- Keep everything with a non-zero balance in the row            )       where@@ -432,7 +441,7 @@         balance = maybeStripPrices . case accountlistmode_ ropts of             ALTree | d == depth -> aibalance             _                   -> aebalance-          where maybeStripPrices = if show_costs_ ropts then id else mixedAmountStripPrices+          where maybeStripPrices = if conversionop_ ropts == Just NoConversionOp then id else mixedAmountStripPrices      -- Accounts interesting because they are a fork for interesting subaccounts     interestingParents = dbg5 "interestingParents" $ case accountlistmode_ ropts of
Hledger/Reports/PostingsReport.hs view
@@ -63,7 +63,7 @@ postingsReport :: ReportSpec -> Journal -> PostingsReport postingsReport rspec@ReportSpec{_rsReportOpts=ropts@ReportOpts{..}} j = items     where-      reportspan  = reportSpanBothDates j rspec+      (reportspan, colspans) = reportSpanBothDates j rspec       whichdate   = whichDate ropts       mdepth      = queryDepth $ _rsQuery rspec       multiperiod = interval_ /= NoInterval@@ -76,7 +76,7 @@         | multiperiod = [(p, Just period) | (p, period) <- summariseps reportps]         | otherwise   = [(p, Nothing) | p <- reportps]         where-          summariseps = summarisePostingsByInterval interval_ whichdate mdepth showempty reportspan+          summariseps = summarisePostingsByInterval whichdate mdepth showempty colspans           showempty = empty_ || average_        -- Posting report items ready for display.@@ -164,15 +164,12 @@ -- | Convert a list of postings into summary postings, one per interval, -- aggregated to the specified depth if any. -- Each summary posting will have a non-Nothing interval end date.-summarisePostingsByInterval :: Interval -> WhichDate -> Maybe Int -> Bool -> DateSpan -> [Posting] -> [SummaryPosting]-summarisePostingsByInterval interval wd mdepth showempty reportspan =+summarisePostingsByInterval :: WhichDate -> Maybe Int -> Bool -> [DateSpan] -> [Posting] -> [SummaryPosting]+summarisePostingsByInterval wd mdepth showempty colspans =     concatMap (\(s,ps) -> summarisePostingsInDateSpan s wd mdepth showempty ps)     -- Group postings into their columns. We try to be efficient, since     -- there can possibly be a very large number of intervals (cf #1683)     . groupByDateSpan showempty (postingDateOrDate2 wd) colspans-  where-    -- The date spans to be included as report columns.-    colspans = splitSpan interval reportspan  -- | Given a date span (representing a report interval) and a list of -- postings within it, aggregate the postings into one summary posting per@@ -332,14 +329,14 @@         let periodexpr `gives` dates = do               j' <- samplejournal               registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j') `is` dates-                  where opts = defreportopts{period_=maybePeriod date1 periodexpr}+                  where opts = defreportopts{period_=Just $ parsePeriodExpr' date1 periodexpr}         ""     `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]         "2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]         "2007" `gives` []         "june" `gives` ["2008/06/01","2008/06/02","2008/06/03"]         "monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"]         "quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"]-        let opts = defreportopts{period_=maybePeriod date1 "yearly"}+        let opts = defreportopts{period_=Just $ parsePeriodExpr' date1 "yearly"}         (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines          ["2008/01/01 - 2008/12/31         assets:bank:saving               $1           $1"          ,"                                assets:cash                     $-2          $-1"@@ -349,9 +346,9 @@          ,"                                income:salary                   $-1          $-1"          ,"                                liabilities:debts                $1            0"          ]-        let opts = defreportopts{period_=maybePeriod date1 "quarterly"}+        let opts = defreportopts{period_=Just $ parsePeriodExpr' date1 "quarterly"}         registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/10/01"]-        let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True}+        let opts = defreportopts{period_=Just $ parsePeriodExpr' date1 "quarterly",empty_=True}         registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]        ]@@ -377,7 +374,7 @@     -}    ,testCase "summarisePostingsByInterval" $-    summarisePostingsByInterval (Quarters 1) PrimaryDate Nothing False (DateSpan Nothing Nothing) [] @?= []+    summarisePostingsByInterval PrimaryDate Nothing False [DateSpan Nothing Nothing] [] @?= []    -- ,tests_summarisePostingsInDateSpan = [     --  "summarisePostingsInDateSpan" ~: do
Hledger/Reports/ReportOptions.hs view
@@ -30,6 +30,7 @@   defreportopts,   rawOptsToReportOpts,   defreportspec,+  setDefaultConversionOp,   reportOptsToSpec,   updateReportSpec,   updateReportSpecWith,@@ -69,7 +70,7 @@ import Data.Either.Extra (eitherToMaybe) import Data.Functor.Identity (Identity(..)) import Data.List.Extra (find, isPrefixOf, nubSort)-import Data.Maybe (fromMaybe, mapMaybe)+import Data.Maybe (fromMaybe, isJust) import qualified Data.Text as T import Data.Time.Calendar (Day, addDays) import Data.Default (Default(..))@@ -124,7 +125,7 @@      period_           :: Period     ,interval_         :: Interval     ,statuses_         :: [Status]  -- ^ Zero, one, or two statuses to be matched-    ,cost_             :: Costing  -- ^ Should we convert amounts to cost, when present?+    ,conversionop_     :: Maybe ConversionOp  -- ^ Which operation should we apply to conversion transactions?     ,value_            :: Maybe ValuationType  -- ^ What value should amounts be converted to ?     ,infer_prices_     :: Bool      -- ^ Infer market prices from transactions ?     ,depth_            :: Maybe Int@@ -180,7 +181,7 @@     { period_           = PeriodAll     , interval_         = NoInterval     , statuses_         = []-    , cost_             = NoCost+    , conversionop_     = Nothing     , value_            = Nothing     , infer_prices_     = False     , depth_            = Nothing@@ -223,7 +224,6 @@      let formatstring = T.pack <$> maybestringopt "format" rawopts         querystring  = map T.pack $ listofstringopt "args" rawopts  -- doesn't handle an arg like "" right-        (costing, valuation) = valuationTypeFromRawOpts rawopts         pretty = fromMaybe False $ alwaysneveropt "pretty" rawopts          format = case parseStringFormat <$> formatstring of@@ -235,8 +235,8 @@           {period_           = periodFromRawOpts d rawopts           ,interval_         = intervalFromRawOpts rawopts           ,statuses_         = statusesFromRawOpts rawopts-          ,cost_             = costing-          ,value_            = valuation+          ,conversionop_     = conversionOpFromRawOpts rawopts+          ,value_            = valuationTypeFromRawOpts rawopts           ,infer_prices_     = boolopt "infer-market-prices" rawopts           ,depth_            = maybeposintopt "depth" rawopts           ,date2_            = boolopt "date2" rawopts@@ -290,6 +290,11 @@     , _rsQueryOpts  = []     } +-- | Set the default ConversionOp.+setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec+setDefaultConversionOp def rspec@ReportSpec{_rsReportOpts=ropts} =+    rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just def}}+ accountlistmodeopt :: RawOpts -> AccountListMode accountlistmodeopt =   fromMaybe ALFlat . choiceopt parse where@@ -469,39 +474,32 @@   | s `elem` ss = ropts{statuses_=filter (/= s) ss}   | otherwise   = ropts{statuses_=simplifyStatuses (s:ss)} --- | Parse the type of valuation and costing to be performed, if any,--- specified by -B/--cost, -V, -X/--exchange, or --value flags. It is--- allowed to combine -B/--cost with any other valuation type. If--- there's more than one valuation type, the rightmost flag wins.--- This will fail with a usage error if an invalid argument is passed--- to --value, or if --valuechange is called with a valuation type--- other than -V/--value=end.-valuationTypeFromRawOpts :: RawOpts -> (Costing, Maybe ValuationType)-valuationTypeFromRawOpts rawopts = case (balancecalcopt rawopts, directcost, directval) of-    (CalcValueChange, _,      Nothing       ) -> (directcost, Just $ AtEnd Nothing)  -- If no valuation requested for valuechange, use AtEnd-    (CalcValueChange, _,      Just (AtEnd _)) -> (directcost, directval)             -- If AtEnd valuation requested, use it-    (CalcValueChange, _,      _             ) -> usageError "--valuechange only produces sensible results with --value=end"-    (CalcGain,        Cost,   _             ) -> usageError "--gain cannot be combined with --cost"-    (CalcGain,        NoCost, Nothing       ) -> (directcost, Just $ AtEnd Nothing)  -- If no valuation requested for gain, use AtEnd-    (_,               _,      _             ) -> (directcost, directval)             -- Otherwise, use requested valuation+-- | Parse the type of valuation to be performed, if any, specified by -V,+-- -X/--exchange, or --value flags. If there's more than one valuation type,+-- the rightmost flag wins. This will fail with a usage error if an invalid+-- argument is passed to --value, or if --valuechange is called with a+-- valuation type other than -V/--value=end.+valuationTypeFromRawOpts :: RawOpts -> Maybe ValuationType+valuationTypeFromRawOpts rawopts = case (balancecalcopt rawopts, directval) of+    (CalcValueChange, Nothing       ) -> Just $ AtEnd Nothing  -- If no valuation requested for valuechange, use AtEnd+    (CalcValueChange, Just (AtEnd _)) -> directval             -- If AtEnd valuation requested, use it+    (CalcValueChange, _             ) -> usageError "--valuechange only produces sensible results with --value=end"+    (CalcGain,        Nothing       ) -> Just $ AtEnd Nothing  -- If no valuation requested for gain, use AtEnd+    (_,               _             ) -> directval             -- Otherwise, use requested valuation   where-    directcost = if Cost `elem` map fst valuationopts then Cost else NoCost-    directval  = lastMay $ mapMaybe snd valuationopts--    valuationopts = collectopts valuationfromrawopt rawopts+    directval = lastMay $ collectopts valuationfromrawopt rawopts     valuationfromrawopt (n,v)  -- option name, value-      | n == "B"     = Just (Cost,   Nothing)  -- keep supporting --value=cost for now-      | n == "V"     = Just (NoCost, Just $ AtEnd Nothing)-      | n == "X"     = Just (NoCost, Just $ AtEnd (Just $ T.pack v))-      | n == "value" = Just $ valueopt v+      | n == "V"     = Just $ AtEnd Nothing+      | n == "X"     = Just $ AtEnd (Just $ T.pack v)+      | n == "value" = valueopt v       | otherwise    = Nothing     valueopt v-      | t `elem` ["cost","c"]  = (Cost,   AtEnd . Just <$> mc)  -- keep supporting --value=cost,COMM for now-      | t `elem` ["then" ,"t"] = (NoCost, Just $ AtThen mc)-      | t `elem` ["end" ,"e"]  = (NoCost, Just $ AtEnd  mc)-      | t `elem` ["now" ,"n"]  = (NoCost, Just $ AtNow  mc)+      | t `elem` ["cost","c"]  = AtEnd . Just <$> mc  -- keep supporting --value=cost,COMM for now+      | t `elem` ["then" ,"t"] = Just $ AtThen mc+      | t `elem` ["end" ,"e"]  = Just $ AtEnd  mc+      | t `elem` ["now" ,"n"]  = Just $ AtNow  mc       | otherwise = case parsedateM t of-            Just d  -> (NoCost, Just $ AtDate d mc)+            Just d  -> Just $ AtDate d mc             Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: then|end|now|t|e|n|YYYY-MM-DD"       where         -- parse --value's value: TYPE[,COMM]@@ -510,6 +508,21 @@                    "" -> Nothing                    c  -> Just $ T.pack c +-- | Parse the type of costing to be performed, if any, specified by -B/--cost+-- or --value flags. If there's more than one costing type, the rightmost flag+-- wins. This will fail with a usage error if an invalid argument is passed to+-- --cost or if a costing type is requested with --gain.+conversionOpFromRawOpts :: RawOpts -> Maybe ConversionOp+conversionOpFromRawOpts rawopts+    | isJust costFlag && balancecalcopt rawopts == CalcGain = usageError "--gain cannot be combined with --cost"+    | otherwise = costFlag+  where+    costFlag = lastMay $ collectopts conversionopfromrawopt rawopts+    conversionopfromrawopt (n,v)  -- option name, value+      | n == "B"                                    = Just ToCost+      | n == "value", takeWhile (/=',') v `elem` ["cost", "c"] = Just ToCost  -- keep supporting --value=cost for now+      | otherwise                                   = Nothing+ -- | Select the Transaction date accessor based on --date2. transactionDateFn :: ReportOpts -> (Transaction -> Day) transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate@@ -578,17 +591,15 @@   where     valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts)     gain      p = maybe id (mixedAmountApplyGain      priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts)-    costing = case cost_ ropts of-        Cost   -> journalToCost-        NoCost -> id+    costing     = journalToCost (fromMaybe NoConversionOp $ conversionop_ ropts)      -- Find the end of the period containing this posting     periodEnd  = addDays (-1) . fromMaybe err . mPeriodEnd . postingDate     mPeriodEnd = case interval_ ropts of-        NoInterval -> const . spanEnd $ reportSpan j rspec+        NoInterval -> const . spanEnd . fst $ reportSpan j rspec         _          -> spanEnd <=< latestSpanContaining (historical : spans)     historical = DateSpan Nothing $ spanStart =<< headMay spans-    spans = splitSpan (interval_ ropts) $ reportSpanBothDates j rspec+    spans = snd $ reportSpanBothDates j rspec     styles = journalCommodityStyles j     err = error "journalApplyValuationFromOpts: expected all spans to have an end date" @@ -605,9 +616,9 @@   where     valuation mc span = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)     gain mc span = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)-    costing = case cost_ ropts of-        Cost   -> styleMixedAmount styles . mixedAmountCost-        NoCost -> id+    costing = case fromMaybe NoConversionOp $ conversionop_ ropts of+        NoConversionOp -> id+        ToCost         -> styleMixedAmount styles . mixedAmountCost     styles = journalCommodityStyles j     err = error "mixedAmountApplyValuationAfterSumFromOptsWith: expected all spans to have an end date" @@ -642,18 +653,20 @@ -- options or queries, or otherwise the earliest and latest transaction or -- posting dates in the journal. If no dates are specified by options/queries -- and the journal is empty, returns the null date span.-reportSpan :: Journal -> ReportSpec -> DateSpan+-- Also return the intervals if they are requested.+reportSpan :: Journal -> ReportSpec -> (DateSpan, [DateSpan]) reportSpan = reportSpanHelper False  -- | Like reportSpan, but uses both primary and secondary dates when calculating -- the span.-reportSpanBothDates :: Journal -> ReportSpec -> DateSpan+reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, [DateSpan]) reportSpanBothDates = reportSpanHelper True  -- | A helper for reportSpan, which takes a Bool indicating whether to use both -- primary and secondary dates.-reportSpanHelper :: Bool -> Journal -> ReportSpec -> DateSpan-reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} = reportspan+reportSpanHelper :: Bool -> Journal -> ReportSpec -> (DateSpan, [DateSpan])+reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} =+    (reportspan, intervalspans)   where     -- The date span specified by -b/-e/-p options and query args if any.     requestedspan  = dbg3 "requestedspan" $ if bothdates then queryDateSpan' query else queryDateSpan (date2_ ropts) query@@ -677,10 +690,10 @@                                               (spanEnd =<< lastMay intervalspans)  reportStartDate :: Journal -> ReportSpec -> Maybe Day-reportStartDate j = spanStart . reportSpan j+reportStartDate j = spanStart . fst . reportSpan j  reportEndDate :: Journal -> ReportSpec -> Maybe Day-reportEndDate j = spanEnd . reportSpan j+reportEndDate j = spanEnd . fst . reportSpan j  -- Some pure alternatives to the above. XXX review/clean up 
Hledger/Utils/Text.hs view
@@ -169,15 +169,17 @@  isSingleQuoted :: Text -> Bool isSingleQuoted s =-  T.length (T.take 2 s) == 2 && T.head s == '\'' && T.last s == '\''+  T.length s >= 2 && T.head s == '\'' && T.last s == '\''  isDoubleQuoted :: Text -> Bool isDoubleQuoted s =-  T.length (T.take 2 s) == 2 && T.head s == '"' && T.last s == '"'+  T.length s >= 2 && T.head s == '"' && T.last s == '"'  textUnbracket :: Text -> Text textUnbracket s-    | (T.head s == '[' && T.last s == ']') || (T.head s == '(' && T.last s == ')') = T.init $ T.tail s+    | T.null s = s+    | T.head s == '[' && T.last s == ']' = T.init $ T.tail s+    | T.head s == '(' && T.last s == ')' = T.init $ T.tail s     | otherwise = s  -- | Join several multi-line strings as side-by-side rectangular strings of the same height, top-padded.
hledger-lib.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           hledger-lib-version:        1.24.99+version:        1.25 synopsis:       A reusable library providing the core functionality of hledger description:    A reusable library containing hledger's core functionality.                 This is used by most hledger* packages so that they support the same